Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/lib/schema/timelineQuerySchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { CHAIN_ID } from '@/lib/consts';
import { IS_TESTNET } from '@/lib/consts';

const timelineQuerySchema = z
.object({
Expand All @@ -16,7 +16,10 @@ const timelineQuerySchema = z
chain_id: z
.string()
.optional()
.transform((v) => (v ? Number(v) : CHAIN_ID)),
.transform((v) => {
if (v) return Number(v);
return IS_TESTNET ? 84532 : null;
}),
hidden: z
.string()
.optional()
Expand Down
2 changes: 1 addition & 1 deletion src/lib/supabase/in_process_moments/getArtistTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const getArtistTimeline = async ({
p_type: type ?? undefined,
p_limit: limit,
p_page: page,
p_chainid: chainId,
p_chainid: chainId as number | undefined,
p_hidden: hidden,
p_mime: mime,
p_period: period,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const getCollectionTimeline = async ({
p_collection: collection,
p_limit: limit,
p_page: page,
p_chainid: chainId,
p_chainid: chainId as number | undefined,
p_hidden: hidden,
p_mime: mime,
p_period: period,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const getInProcessTimeline = async ({
const { data, error } = await supabase.rpc('get_in_process_timeline', {
p_limit: limit,
p_page: page,
p_chainid: chainId,
p_chainid: chainId as number | undefined,
p_hidden: hidden,
p_mime: mime,
p_period: period,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
-- When p_chainid is NULL (not provided by caller), restrict to prod chains only (1, 8453).
-- Passing an explicit chain_id still filters to that single chain.

-- ── get_in_process_timeline ──────────────────────────────────────────────────
DROP FUNCTION IF EXISTS public.get_in_process_timeline(integer, integer, numeric, boolean, text, text, text, boolean);

CREATE OR REPLACE FUNCTION public.get_in_process_timeline(
p_limit integer DEFAULT 100,
p_page integer DEFAULT 1,
p_chainid numeric DEFAULT NULL,
p_hidden boolean DEFAULT false,
p_mime text DEFAULT NULL,
p_period text DEFAULT NULL,
p_channel text DEFAULT NULL,
p_curated boolean DEFAULT false
)
RETURNS json
LANGUAGE plpgsql
AS $function$
DECLARE
capped_limit int := GREATEST(1, LEAST(COALESCE(NULLIF(p_limit, 0), 100), 1000));
clamped_page int := GREATEST(1, COALESCE(NULLIF(p_page, 0), 1));
offset_val int := (clamped_page - 1) * capped_limit;
v_moments json;
v_total_count int;
BEGIN
WITH
filtered_ids AS (
SELECT DISTINCT m.id, m.token_id, m.created_at
FROM in_process_moments m
INNER JOIN in_process_collections c ON m.collection = c.id
INNER JOIN in_process_artists da ON c.creator = da.address
LEFT JOIN in_process_metadata meta ON meta.moment = m.id
WHERE
(p_chainid IS NOT NULL AND c.chain_id = p_chainid OR p_chainid IS NULL AND c.chain_id IN (1, 8453))
AND (p_mime IS NULL OR meta.content->>'mime' LIKE p_mime)
AND moment_matches_period(m.created_at, p_period)
AND moment_matches_channel(m.id, p_channel)
AND moment_is_visible(m.collection, m.token_id, p_hidden)
AND (NOT p_curated OR (da.username IS NOT NULL AND da.username != ''))
),
total AS (SELECT COUNT(*) AS cnt FROM filtered_ids),
paged_ids AS (
SELECT id FROM filtered_ids
ORDER BY created_at DESC, token_id DESC
LIMIT capped_limit OFFSET offset_val
),
moment_data AS (
SELECT DISTINCT
m.id,
m.collection,
m.token_id,
m.uri,
m.created_at,
c.address,
c.chain_id,
c.protocol,
c.creator,
da.username AS creator_username,
get_creator_hidden(m.collection, m.token_id, c.creator) AS creator_hidden,
to_jsonb(meta) AS metadata
FROM paged_ids pi
INNER JOIN in_process_moments m ON m.id = pi.id
INNER JOIN in_process_collections c ON m.collection = c.id
INNER JOIN in_process_artists da ON c.creator = da.address
LEFT JOIN in_process_metadata meta ON meta.moment = m.id
)
SELECT
(SELECT cnt FROM total),
json_agg(
build_moment_json(
md.address, md.token_id, md.chain_id, md.protocol::text, md.id, md.uri,
md.creator, md.creator_username, md.creator_hidden,
md.collection, md.metadata::json, md.created_at
)
ORDER BY md.created_at DESC, md.token_id DESC
)
INTO v_total_count, v_moments
FROM moment_data md;

RETURN build_timeline_result(v_moments, v_total_count, capped_limit, clamped_page);
END;
$function$;

-- ── get_artist_timeline ──────────────────────────────────────────────────────
DROP FUNCTION IF EXISTS public.get_artist_timeline(text, text, integer, integer, numeric, boolean, text, text, text, boolean);

CREATE OR REPLACE FUNCTION public.get_artist_timeline(
p_artist text,
p_type text DEFAULT NULL,
p_limit integer DEFAULT 100,
p_page integer DEFAULT 1,
p_chainid numeric DEFAULT NULL,
p_hidden boolean DEFAULT false,
p_mime text DEFAULT NULL,
p_period text DEFAULT NULL,
p_channel text DEFAULT NULL,
p_curated boolean DEFAULT false
)
RETURNS json
LANGUAGE plpgsql
AS $function$
DECLARE
capped_limit int := GREATEST(1, LEAST(COALESCE(NULLIF(p_limit, 0), 100), 1000));
clamped_page int := GREATEST(1, COALESCE(NULLIF(p_page, 0), 1));
offset_val int := (clamped_page - 1) * capped_limit;
v_moments json;
v_total_count int;
v_artist_address text;
BEGIN
SELECT address INTO v_artist_address
FROM in_process_artists
WHERE address = LOWER(p_artist)
OR username ILIKE p_artist
LIMIT 1;

IF v_artist_address IS NULL THEN
RETURN build_timeline_result(NULL, 0, capped_limit, clamped_page);
END IF;

WITH
filtered_ids AS (
SELECT DISTINCT m.id, m.token_id, m.created_at
FROM in_process_moments m
INNER JOIN in_process_collections c ON m.collection = c.id
INNER JOIN in_process_artists da ON c.creator = da.address
LEFT JOIN in_process_metadata meta ON meta.moment = m.id
WHERE
(p_chainid IS NOT NULL AND c.chain_id = p_chainid OR p_chainid IS NULL AND c.chain_id IN (1, 8453))
AND (p_mime IS NULL OR meta.content->>'mime' LIKE p_mime)
AND moment_matches_period(m.created_at, p_period)
AND moment_matches_channel(m.id, p_channel)
AND (NOT p_curated OR (da.username IS NOT NULL AND da.username != ''))
AND (
(
(p_type IS NULL OR p_type = 'default')
AND c.creator = v_artist_address
AND (p_hidden = true OR get_creator_hidden(m.collection, m.token_id, c.creator) = false)
)
OR
(
(p_type IS NULL OR p_type = 'mutual')
AND c.creator != v_artist_address
AND EXISTS (
SELECT 1 FROM in_process_admins adm_check
WHERE adm_check.collection = m.collection
AND (adm_check.token_id = m.token_id OR adm_check.token_id = 0)
AND adm_check.artist_address = v_artist_address
)
AND (p_hidden = true OR get_creator_hidden(m.collection, m.token_id, v_artist_address) = false)
)
)
),
total AS (SELECT COUNT(*) AS cnt FROM filtered_ids),
paged_ids AS (
SELECT id FROM filtered_ids
ORDER BY created_at DESC, token_id DESC
LIMIT capped_limit OFFSET offset_val
),
moment_data AS (
SELECT DISTINCT
m.id,
m.collection,
m.token_id,
m.uri,
m.created_at,
c.address,
c.chain_id,
c.protocol,
c.creator,
da.username AS creator_username,
get_creator_hidden(m.collection, m.token_id, c.creator) AS creator_hidden,
to_jsonb(meta) AS metadata
FROM paged_ids pi
INNER JOIN in_process_moments m ON m.id = pi.id
INNER JOIN in_process_collections c ON m.collection = c.id
INNER JOIN in_process_artists da ON c.creator = da.address
LEFT JOIN in_process_metadata meta ON meta.moment = m.id
)
SELECT
(SELECT cnt FROM total),
json_agg(
build_moment_json(
md.address, md.token_id, md.chain_id, md.protocol::text, md.id, md.uri,
md.creator, md.creator_username, md.creator_hidden,
md.collection, md.metadata::json, md.created_at
)
ORDER BY md.created_at DESC, md.token_id DESC
)
INTO v_total_count, v_moments
FROM moment_data md;

RETURN build_timeline_result(v_moments, v_total_count, capped_limit, clamped_page);
END;
$function$;

-- ── get_collection_timeline ──────────────────────────────────────────────────
DROP FUNCTION IF EXISTS public.get_collection_timeline(text, integer, integer, numeric, boolean, text, text, text, text, boolean);

CREATE OR REPLACE FUNCTION public.get_collection_timeline(
p_collection text,
p_limit integer DEFAULT 100,
p_page integer DEFAULT 1,
p_chainid numeric DEFAULT NULL,
p_hidden boolean DEFAULT false,
p_mime text DEFAULT NULL,
p_period text DEFAULT NULL,
p_channel text DEFAULT NULL,
p_artist text DEFAULT NULL,
p_curated boolean DEFAULT false
)
RETURNS json
LANGUAGE plpgsql
AS $function$
DECLARE
capped_limit int := GREATEST(1, LEAST(COALESCE(NULLIF(p_limit, 0), 100), 1000));
clamped_page int := GREATEST(1, COALESCE(NULLIF(p_page, 0), 1));
offset_val int := (clamped_page - 1) * capped_limit;
v_moments json;
v_total_count int;
BEGIN
WITH
filtered_ids AS (
SELECT DISTINCT m.id, m.token_id, m.created_at
FROM in_process_moments m
INNER JOIN in_process_collections c ON m.collection = c.id
INNER JOIN in_process_artists da ON c.creator = da.address
LEFT JOIN in_process_metadata meta ON meta.moment = m.id
WHERE
c.address = LOWER(p_collection)
AND (p_chainid IS NOT NULL AND c.chain_id = p_chainid OR p_chainid IS NULL AND c.chain_id IN (1, 8453))
AND (p_mime IS NULL OR meta.content->>'mime' LIKE p_mime)
AND moment_matches_period(m.created_at, p_period)
AND moment_matches_channel(m.id, p_channel)
AND (p_artist IS NULL OR da.username ILIKE p_artist OR da.address = LOWER(p_artist))
AND moment_is_visible(m.collection, m.token_id, p_hidden)
AND (NOT p_curated OR (da.username IS NOT NULL AND da.username != ''))
),
total AS (SELECT COUNT(*) AS cnt FROM filtered_ids),
paged_ids AS (
SELECT id FROM filtered_ids
ORDER BY created_at DESC, token_id DESC
LIMIT capped_limit OFFSET offset_val
),
moment_data AS (
SELECT
m.id,
m.collection,
m.token_id,
m.uri,
m.created_at,
c.address,
c.chain_id,
c.protocol,
c.creator,
da.username AS creator_username,
get_creator_hidden(m.collection, m.token_id, c.creator) AS creator_hidden,
to_jsonb(meta) AS metadata
FROM paged_ids pi
INNER JOIN in_process_moments m ON m.id = pi.id
INNER JOIN in_process_collections c ON m.collection = c.id
INNER JOIN in_process_artists da ON c.creator = da.address
LEFT JOIN in_process_metadata meta ON meta.moment = m.id
)
SELECT
(SELECT cnt FROM total),
json_agg(
build_moment_json(
md.address, md.token_id, md.chain_id, md.protocol::text, md.id, md.uri,
md.creator, md.creator_username, md.creator_hidden,
md.collection, md.metadata::json, md.created_at
)
ORDER BY md.created_at DESC, md.token_id DESC
)
INTO v_total_count, v_moments
FROM moment_data md;

RETURN build_timeline_result(v_moments, v_total_count, capped_limit, clamped_page);
END;
$function$;
2 changes: 1 addition & 1 deletion src/lib/timeline/__tests__/getTimelineHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import getTimelineHandler from '@/lib/timeline/getTimelineHandler';
const BASE_PARAMS = {
limit: 10,
page: 1,
chainId: 8453,
chainId: null as number | null,
hidden: false,
mime: undefined as string | undefined,
period: undefined as string | undefined,
Expand Down
18 changes: 18 additions & 0 deletions src/lib/timeline/__tests__/validateTimelineQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ describe('validateTimelineQuery', () => {
});
});

describe('chain_id param', () => {
it('returns null when chain_id not provided (prod default — all prod chains)', () => {
const result = validateTimelineQuery(makeRequest());
// IS_TESTNET=false in test env → null signals "all prod chains" to the RPC
expect((result as any).chainId).toBeNull();
});

it('returns the provided chain_id when explicitly set to Base', () => {
const result = validateTimelineQuery(makeRequest({ chain_id: '8453' }));
expect((result as any).chainId).toBe(8453);
});

it('returns chain_id=1 when explicitly set to Ethereum mainnet', () => {
const result = validateTimelineQuery(makeRequest({ chain_id: '1' }));
expect((result as any).chainId).toBe(1);
});
});

describe('type param', () => {
it('accepts "mutual"', () => {
const result = validateTimelineQuery(makeRequest({ type: 'mutual' }));
Expand Down
2 changes: 1 addition & 1 deletion src/lib/timeline/getTimelineHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface TimelineParams {
page: number;
collection?: string;
artist?: string;
chainId: number;
chainId: number | null;
hidden: boolean;
mime?: string;
period?: string;
Expand Down
6 changes: 3 additions & 3 deletions src/types/moment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export interface TimelinePagination {
export interface GetInProcessTimelineParams {
limit?: number;
page?: number;
chainId?: number;
chainId?: number | null;
hidden?: boolean;
mime?: string;
period?: string;
Expand All @@ -83,7 +83,7 @@ export interface GetArtistTimelineParams {
type?: 'mutual' | 'default' | null;
limit?: number;
page?: number;
chainId?: number;
chainId?: number | null;
hidden?: boolean;
mime?: string;
period?: string;
Expand All @@ -100,7 +100,7 @@ export interface GetCollectionTimelineParams {
collection: string;
limit?: number;
page?: number;
chainId?: number;
chainId?: number | null;
hidden?: boolean;
mime?: string;
period?: string;
Expand Down
Loading