@gigamusic/links 0.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,34 +1,66 @@
1
1
  # @gigamusic/links
2
2
 
3
- Linktree-style per-release link pages for `@gigamusic/*` artist sites. Ships its own Prisma fragment (`LinkPage`, `LinkPageItem`), a public `LinkPageView` component, admin page bodies + form, admin API handler factories, and platform-detection helpers.
4
-
5
- ## Install
3
+ Linktree-style per-release link pages for the gigamusic platform. Ships a Drizzle schema (`linkPages`, `linkPageItems`), a typed queries factory, admin API handler factories, platform-detection helpers, and a small SVG `LinkPlatformIcon` component.
6
4
 
7
5
  ```ts
8
6
  import {
9
7
  createLinkPageQueries,
10
- LinkPageView,
11
- createPublicLinkPage,
12
- createAdminLinkPagesHandlers,
13
8
  detectLinkPlatform,
9
+ PLATFORM_LABELS,
14
10
  LinkPlatformIcon,
11
+ type KnownLinkPlatform,
15
12
  } from "@gigamusic/links";
13
+
14
+ // Admin handler factories live on the /server subpath
15
+ import {
16
+ createAdminLinkPagesHandlers,
17
+ createAdminLinkPageByIdHandlers,
18
+ createAdminLinkPageItemsHandlers,
19
+ } from "@gigamusic/links/server";
16
20
  ```
17
21
 
18
- The package is source-shipped add `@gigamusic/links` to `transpilePackages` in your `next.config.ts` (the create-gigamusic-app scaffold does this automatically).
22
+ The public link-page UI is the consumer's responsibility fetch with `createLinkPageQueries(db).getPublicLinkPageBySlug(slug)` and render whatever you want.
19
23
 
20
- ## Prisma fragment
24
+ ## Schema subpath
21
25
 
22
- Run `gigamusic-links sync` to copy the `link-page.prisma` fragment into `prisma/schema/20-link-page.prisma`. Run after every `@gigamusic/links` upgrade; `gigamusic-links doctor` checks for staleness.
26
+ `@gigamusic/links/schema` exports the table objects and relations:
27
+
28
+ ```ts
29
+ import {
30
+ linkPages,
31
+ linkPageItems,
32
+ linkPagesRelations,
33
+ linkPageItemsRelations,
34
+ linkPageColumns,
35
+ linkPageItemColumns,
36
+ } from "@gigamusic/links/schema";
37
+ ```
23
38
 
24
- The fragment references the `Release` model owned by `@gigamusic/db`. Prisma requires both sides of a relation to be declared, so consumers must add the inverse on their copy of `Release`:
39
+ Merge with `@gigamusic/db/schema` when constructing your Drizzle instance:
40
+
41
+ ```ts
42
+ import { drizzle } from "drizzle-orm/node-postgres";
43
+ import { Pool } from "pg";
44
+ import * as dbSchema from "@gigamusic/db/schema";
45
+ import * as linksSchema from "@gigamusic/links/schema";
46
+
47
+ const schema = { ...dbSchema, ...linksSchema };
48
+ export const db = drizzle(new Pool({ connectionString: process.env.DATABASE_URL }), { schema });
49
+ ```
50
+
51
+ ## Release back-reference
52
+
53
+ `linkPages.releaseId` is a nullable FK to `releases.id` from `@gigamusic/db`. Drizzle relations are declared independently on each side, so this package's `linkPagesRelations` already declares the `linkPage.release` direction. If you want a `release.linkPages` back-reference (for `db.query.releases.findFirst({ with: { linkPages: true } })`), add it in your own schema file by composing onto the base `releasesRelations`:
54
+
55
+ ```ts
56
+ import { relations } from "drizzle-orm";
57
+ import { releases } from "@gigamusic/db/schema";
58
+ import { linkPages } from "@gigamusic/links/schema";
25
59
 
26
- ```prisma
27
- // prisma/schema/99-app.prisma (or wherever your Release lives)
28
- model Release {
29
- // ...existing fields...
30
- linkPages LinkPage[]
31
- }
60
+ export const releasesRelations = relations(releases, ({ many }) => ({
61
+ // ...keep the existing many() relations from @gigamusic/db/schema if you need them...
62
+ linkPages: many(linkPages),
63
+ }));
32
64
  ```
33
65
 
34
- If your `Release` is shipped verbatim from `@gigamusic/db`'s `10-release.prisma`, add a thin override file at `99-release-link-pages.prisma` that re-declares the model with the extra field, or extend the upstream fragment when re-syncing.
66
+ Because relations are TS modules, you compose them once and never re-apply patches on package upgrades.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gigamusic/links",
3
- "version": "0.3.0",
4
- "description": "Editable Linktree-style link pages for gigamusic artist sites. Ships a Prisma fragment, public + admin React components, admin API handler factories, and platform-detection helpers.",
3
+ "version": "2.0.0",
4
+ "description": "Editable Linktree-style link pages for gigamusic artist sites. Ships a Drizzle schema, admin API handler factories, and platform-detection helpers.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -9,68 +9,53 @@
9
9
  "directory": "packages/links"
10
10
  },
11
11
  "type": "module",
12
- "main": "./src/index.ts",
13
- "types": "./src/index.ts",
14
12
  "exports": {
15
13
  ".": {
16
14
  "types": "./src/index.ts",
17
15
  "default": "./src/index.ts"
18
16
  },
19
- "./client": {
20
- "types": "./src/client.ts",
21
- "default": "./src/client.ts"
22
- },
23
17
  "./server": {
24
18
  "types": "./src/server.ts",
25
19
  "default": "./src/server.ts"
20
+ },
21
+ "./schema": {
22
+ "types": "./src/schema/index.ts",
23
+ "default": "./src/schema/index.ts"
26
24
  }
27
25
  },
28
- "bin": {
29
- "gigamusic-links": "./dist/cli.js"
30
- },
31
26
  "files": [
32
27
  "src",
33
- "prisma",
34
- "dist",
35
28
  "README.md"
36
29
  ],
37
30
  "publishConfig": {
38
31
  "access": "public"
39
32
  },
40
33
  "dependencies": {
41
- "@gigamusic/db": "0.3.0",
42
- "@gigamusic/config": "0.3.0",
43
- "@gigamusic/ui": "0.3.0",
44
- "@gigamusic/core": "0.3.0"
34
+ "@gigamusic/core": "1.0.0",
35
+ "@gigamusic/db": "2.0.0"
45
36
  },
46
37
  "peerDependencies": {
47
- "@prisma/client": ">=6",
38
+ "drizzle-orm": ">=0.45",
48
39
  "next": ">=15",
49
40
  "react": ">=18 <20",
50
41
  "react-dom": ">=18 <20"
51
42
  },
52
43
  "devDependencies": {
53
- "@prisma/client": "^6.1.0",
54
- "@testing-library/dom": "^10.4.0",
55
- "@testing-library/react": "^16.1.0",
56
44
  "@types/node": "^22.10.5",
45
+ "@types/pg": "^8.11.10",
57
46
  "@types/react": "^19.0.0",
58
- "@types/react-dom": "^19.0.0",
59
- "jsdom": "^25.0.1",
47
+ "drizzle-kit": "^0.31.10",
48
+ "drizzle-orm": "^0.45.2",
60
49
  "next": "^16.0.0",
50
+ "pg": "^8.21.0",
61
51
  "react": "^19.0.0",
62
- "react-dom": "^19.0.0",
63
- "tsup": "^8.3.5",
64
52
  "typescript": "^5.7.3",
65
53
  "vitest": "^2.1.8"
66
54
  },
67
55
  "scripts": {
68
- "build": "pnpm run prisma:generate && tsup",
69
- "dev": "tsup --watch",
70
56
  "lint": "eslint src",
71
- "test": "pnpm run prisma:generate && vitest run",
72
- "typecheck": "pnpm run prisma:generate && tsc --noEmit",
73
- "prisma:generate": "prisma generate --schema=./prisma",
74
- "clean": "rm -rf dist .turbo *.tsbuildinfo"
57
+ "test": "vitest run",
58
+ "typecheck": "tsc --noEmit",
59
+ "clean": "rm -rf .turbo *.tsbuildinfo"
75
60
  }
76
61
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- // Server-safe public entry. Everything that requires the React client runtime
2
- // (context providers, the form components, etc.) lives in `./client`.
1
+ // Public entry server-safe types, queries, and platform helpers.
2
+ // Admin handler factories live on the `./server` subpath.
3
3
 
4
4
  // ── Types ────────────────────────────────────────────────────────────────
5
5
  export type {
@@ -14,62 +14,13 @@ export type {
14
14
 
15
15
  // ── Queries factory ─────────────────────────────────────────────────────
16
16
  export { createLinkPageQueries } from "./queries/link-pages";
17
+ export type { LinkPagesDb } from "./queries/link-pages";
17
18
 
18
19
  // ── Platform detection ──────────────────────────────────────────────────
19
20
  export { detectLinkPlatform, PLATFORM_LABELS } from "./platforms/detect";
20
21
  export type { KnownLinkPlatform } from "./platforms/detect";
21
22
  export { LinkPlatformIcon } from "./platforms/icon";
22
23
 
23
- // ── Slots ────────────────────────────────────────────────────────────────
24
- export type {
25
- LinksSlots,
26
- LinkButtonProps,
27
- LinkPageHeaderProps,
28
- } from "./slots";
29
-
30
- // ── Public render surface ───────────────────────────────────────────────
31
- export { LinkPageView } from "./components/LinkPageView";
32
- export { DefaultLinkButton } from "./components/LinkButton";
33
- export { DefaultLinkPageHeader } from "./components/LinkPageHeader";
34
-
35
- // ── Public page (per-slug landing) ──────────────────────────────────────
36
- export {
37
- PublicLinkPage,
38
- createPublicLinkPage,
39
- createPublicLinkPageMetadata,
40
- publicLinkPageMetadata,
41
- registerLinkPageMetadata,
42
- } from "./pages/PublicLinkPage";
43
- export type {
44
- PublicLinkPageProps,
45
- CreatePublicLinkPageDeps,
46
- PublicLinkPageMetadataDeps,
47
- PublicLinkPageMetadataArgs,
48
- } from "./pages/PublicLinkPage";
49
-
50
- // ── Admin page bodies ───────────────────────────────────────────────────
51
- export {
52
- AdminLinkPagesIndexPage,
53
- createAdminLinkPagesIndexPage,
54
- } from "./pages/AdminLinkPagesIndexPage";
55
- export type { AdminLinkPagesIndexPageProps } from "./pages/AdminLinkPagesIndexPage";
56
- export { AdminNewLinkPagePage } from "./pages/AdminNewLinkPagePage";
57
- export type { AdminNewLinkPagePageProps } from "./pages/AdminNewLinkPagePage";
58
- export { AdminEditLinkPagePage } from "./pages/AdminEditLinkPagePage";
59
- export type { AdminEditLinkPagePageProps } from "./pages/AdminEditLinkPagePage";
60
-
61
- // ── Admin form types (so consumers can build their own page wrappers) ───
62
- export type {
63
- LinkPageFormPage,
64
- LinkPageFormItem,
65
- LinkPageFormReleaseOption,
66
- } from "./components/admin/LinkPageForm";
67
- export type { NewLinkPageReleaseOption } from "./components/admin/NewLinkPageForm";
68
-
69
- // Admin API handler factories live on the `./server` subpath — they import
70
- // `next/cache`, which Webpack/Turbopack refuse to bundle into a client tree.
71
- // Use `import { createAdminLinkPagesHandlers } from "@gigamusic/links/server"`.
72
-
73
24
  // ── Slug validation helpers ─────────────────────────────────────────────
74
25
  export {
75
26
  SLUG_PATTERN,
@@ -1,6 +1,13 @@
1
- import type { KnownPlatform } from "@gigamusic/config";
2
-
3
- export type KnownLinkPlatform = KnownPlatform;
1
+ export type KnownLinkPlatform =
2
+ | "spotify"
3
+ | "apple-music"
4
+ | "youtube"
5
+ | "soundcloud"
6
+ | "instagram"
7
+ | "facebook"
8
+ | "tiktok"
9
+ | "x"
10
+ | "bandcamp";
4
11
 
5
12
  interface HostPattern {
6
13
  platform: KnownLinkPlatform;
@@ -1,4 +1,8 @@
1
- import type { PrismaClient } from "@prisma/client";
1
+ import { and, asc, desc, eq, max } from "drizzle-orm";
2
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+ import type * as gigamusicDbSchema from "@gigamusic/db/schema";
4
+ import type * as linksSchema from "../schema/index.js";
5
+ import { linkPageItems, linkPages } from "../schema/index.js";
2
6
  import type {
3
7
  LinkPage,
4
8
  LinkPageInput,
@@ -8,38 +12,42 @@ import type {
8
12
  LinkPageWithItems,
9
13
  } from "../types";
10
14
 
15
+ type RequiredSchema = typeof gigamusicDbSchema & typeof linksSchema;
16
+
17
+ /** Drizzle DB type the link-pages factory needs — a superset that includes both link-pages tables and `releases`. */
18
+ export type LinkPagesDb = NodePgDatabase<RequiredSchema>;
19
+
11
20
  /**
12
- * Factory that binds a `PrismaClient` into the `LinkPageQueries` contract.
21
+ * Factory that binds a Drizzle database into the `LinkPageQueries` contract.
13
22
  * Consumers typically merge the result with `@gigamusic/db`'s `createQueries`.
14
23
  *
15
- * Both inputs and outputs use numeric IDs, matching the underlying
16
- * `Int @id @default(autoincrement())` schema fragments.
24
+ * The `db` instance must have been built with a schema that includes the
25
+ * union of `@gigamusic/db/schema` and `@gigamusic/links/schema`; otherwise
26
+ * the relational `with: { release: ... }` clause won't resolve at runtime.
17
27
  */
18
- export function createLinkPageQueries(db: PrismaClient): LinkPageQueries {
28
+ export function createLinkPageQueries(db: LinkPagesDb): LinkPageQueries {
19
29
  return {
20
30
  listPublishedLinkPages: async (): Promise<LinkPage[]> => {
21
- const rows = await db.linkPage.findMany({
22
- where: { isPublished: true },
23
- orderBy: { updatedAt: "desc" },
31
+ const rows = await db.query.linkPages.findMany({
32
+ where: eq(linkPages.isPublished, true),
33
+ orderBy: desc(linkPages.updatedAt),
24
34
  });
25
- return rows as unknown as LinkPage[];
35
+ return rows;
26
36
  },
27
37
 
28
38
  listAllLinkPages: async (): Promise<LinkPage[]> => {
29
- const rows = await db.linkPage.findMany({
30
- orderBy: { updatedAt: "desc" },
39
+ const rows = await db.query.linkPages.findMany({
40
+ orderBy: desc(linkPages.updatedAt),
31
41
  });
32
- return rows as unknown as LinkPage[];
42
+ return rows;
33
43
  },
34
44
 
35
- getPublicLinkPageBySlug: async (
36
- slug: string,
37
- ): Promise<LinkPageWithItems | null> => {
38
- const row = await db.linkPage.findFirst({
39
- where: { slug, isPublished: true },
40
- include: {
45
+ getPublicLinkPageBySlug: async (slug: string): Promise<LinkPageWithItems | null> => {
46
+ const row = await db.query.linkPages.findFirst({
47
+ where: and(eq(linkPages.slug, slug), eq(linkPages.isPublished, true)),
48
+ with: {
41
49
  release: {
42
- select: {
50
+ columns: {
43
51
  id: true,
44
52
  name: true,
45
53
  slug: true,
@@ -48,20 +56,20 @@ export function createLinkPageQueries(db: PrismaClient): LinkPageQueries {
48
56
  },
49
57
  },
50
58
  items: {
51
- where: { isVisible: true },
52
- orderBy: { position: "asc" },
59
+ where: eq(linkPageItems.isVisible, true),
60
+ orderBy: asc(linkPageItems.position),
53
61
  },
54
62
  },
55
63
  });
56
- return row as unknown as LinkPageWithItems | null;
64
+ return (row ?? null) as unknown as LinkPageWithItems | null;
57
65
  },
58
66
 
59
67
  getLinkPageById: async (id: number): Promise<LinkPageWithItems | null> => {
60
- const row = await db.linkPage.findUnique({
61
- where: { id },
62
- include: {
68
+ const row = await db.query.linkPages.findFirst({
69
+ where: eq(linkPages.id, id),
70
+ with: {
63
71
  release: {
64
- select: {
72
+ columns: {
65
73
  id: true,
66
74
  name: true,
67
75
  slug: true,
@@ -69,67 +77,60 @@ export function createLinkPageQueries(db: PrismaClient): LinkPageQueries {
69
77
  isPublished: true,
70
78
  },
71
79
  },
72
- items: { orderBy: { position: "asc" } },
80
+ items: { orderBy: asc(linkPageItems.position) },
73
81
  },
74
82
  });
75
- return row as unknown as LinkPageWithItems | null;
83
+ return (row ?? null) as unknown as LinkPageWithItems | null;
76
84
  },
77
85
 
78
86
  createLinkPage: async (input: LinkPageInput): Promise<LinkPage> => {
79
- const row = await db.linkPage.create({
80
- data: {
87
+ const [row] = await db
88
+ .insert(linkPages)
89
+ .values({
81
90
  title: input.title,
82
91
  slug: input.slug,
83
92
  description: input.description ?? null,
84
93
  releaseId: input.releaseId ?? null,
85
94
  coverImageUrl: input.coverImageUrl ?? null,
86
95
  isPublished: input.isPublished ?? true,
87
- },
88
- });
89
- return row as unknown as LinkPage;
96
+ })
97
+ .returning();
98
+ return row!;
90
99
  },
91
100
 
92
- updateLinkPage: async (
93
- id: number,
94
- input: Partial<LinkPageInput>,
95
- ): Promise<LinkPage> => {
101
+ updateLinkPage: async (id: number, input: Partial<LinkPageInput>): Promise<LinkPage> => {
96
102
  const data: Record<string, unknown> = {};
97
103
  if (input.title !== undefined) data.title = input.title;
98
104
  if (input.slug !== undefined) data.slug = input.slug;
99
105
  if (input.description !== undefined) data.description = input.description;
100
106
  if (input.releaseId !== undefined) data.releaseId = input.releaseId;
101
- if (input.coverImageUrl !== undefined)
102
- data.coverImageUrl = input.coverImageUrl;
107
+ if (input.coverImageUrl !== undefined) data.coverImageUrl = input.coverImageUrl;
103
108
  if (input.isPublished !== undefined) data.isPublished = input.isPublished;
104
- const row = await db.linkPage.update({
105
- where: { id },
106
- data,
107
- });
108
- return row as unknown as LinkPage;
109
+ const [row] = await db.update(linkPages).set(data).where(eq(linkPages.id, id)).returning();
110
+ return row!;
109
111
  },
110
112
 
111
113
  deleteLinkPage: async (id: number): Promise<void> => {
112
- await db.linkPage.delete({ where: { id } });
114
+ await db.delete(linkPages).where(eq(linkPages.id, id));
113
115
  },
114
116
 
115
- addLinkPageItem: async (
116
- pageId: number,
117
- input: LinkPageItemInput,
118
- ): Promise<LinkPageItem> => {
119
- const max = await db.linkPageItem.aggregate({
120
- where: { pageId },
121
- _max: { position: true },
122
- });
123
- const row = await db.linkPageItem.create({
124
- data: {
117
+ addLinkPageItem: async (pageId: number, input: LinkPageItemInput): Promise<LinkPageItem> => {
118
+ const [maxRow] = await db
119
+ .select({ max: max(linkPageItems.position) })
120
+ .from(linkPageItems)
121
+ .where(eq(linkPageItems.pageId, pageId));
122
+ const nextPosition = input.position ?? ((maxRow?.max ?? -1) + 1);
123
+ const [row] = await db
124
+ .insert(linkPageItems)
125
+ .values({
125
126
  pageId,
126
127
  title: input.title,
127
128
  url: input.url,
128
- position: input.position ?? (max._max.position ?? -1) + 1,
129
+ position: nextPosition,
129
130
  isVisible: input.isVisible ?? true,
130
- },
131
- });
132
- return row as unknown as LinkPageItem;
131
+ })
132
+ .returning();
133
+ return row!;
133
134
  },
134
135
 
135
136
  updateLinkPageItem: async (
@@ -141,15 +142,12 @@ export function createLinkPageQueries(db: PrismaClient): LinkPageQueries {
141
142
  if (input.url !== undefined) data.url = input.url;
142
143
  if (input.position !== undefined) data.position = input.position;
143
144
  if (input.isVisible !== undefined) data.isVisible = input.isVisible;
144
- const row = await db.linkPageItem.update({
145
- where: { id },
146
- data,
147
- });
148
- return row as unknown as LinkPageItem;
145
+ const [row] = await db.update(linkPageItems).set(data).where(eq(linkPageItems.id, id)).returning();
146
+ return row!;
149
147
  },
150
148
 
151
149
  deleteLinkPageItem: async (id: number): Promise<void> => {
152
- await db.linkPageItem.delete({ where: { id } });
150
+ await db.delete(linkPageItems).where(eq(linkPageItems.id, id));
153
151
  },
154
152
 
155
153
  /**
@@ -158,18 +156,15 @@ export function createLinkPageQueries(db: PrismaClient): LinkPageQueries {
158
156
  * their current position — callers must include the full set to fully
159
157
  * resort.
160
158
  */
161
- reorderLinkPageItems: async (
162
- pageId: number,
163
- orderedItemIds: number[],
164
- ): Promise<void> => {
165
- await db.$transaction(
166
- orderedItemIds.map((itemId, position) =>
167
- db.linkPageItem.update({
168
- where: { id: itemId, pageId },
169
- data: { position },
170
- }),
171
- ),
172
- );
159
+ reorderLinkPageItems: async (pageId: number, orderedItemIds: number[]): Promise<void> => {
160
+ await db.transaction(async (tx) => {
161
+ for (let position = 0; position < orderedItemIds.length; position++) {
162
+ await tx
163
+ .update(linkPageItems)
164
+ .set({ position })
165
+ .where(and(eq(linkPageItems.id, orderedItemIds[position]!), eq(linkPageItems.pageId, pageId)));
166
+ }
167
+ });
173
168
  },
174
169
  };
175
170
  }
@@ -0,0 +1,2 @@
1
+ export * from "./link-pages.js";
2
+ export * from "./relations.js";
@@ -0,0 +1,41 @@
1
+ import { boolean, index, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+ import { releases } from "@gigamusic/db/schema";
3
+
4
+ export const linkPageColumns = {
5
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
6
+ slug: text("slug").notNull().unique(),
7
+ title: text("title").notNull(),
8
+ description: text("description"),
9
+ coverImageUrl: text("coverImageUrl"),
10
+ releaseId: integer("releaseId").references(() => releases.id, { onDelete: "set null" }),
11
+ isPublished: boolean("isPublished").notNull().default(true),
12
+ createdAt: timestamp("createdAt", { mode: "date", precision: 3 }).notNull().defaultNow(),
13
+ updatedAt: timestamp("updatedAt", { mode: "date", precision: 3 })
14
+ .notNull()
15
+ .$defaultFn(() => new Date())
16
+ .$onUpdateFn(() => new Date()),
17
+ } as const;
18
+
19
+ export const linkPages = pgTable("link_pages", linkPageColumns, (t) => [
20
+ index("link_pages_releaseId_idx").on(t.releaseId),
21
+ ]);
22
+
23
+ export const linkPageItemColumns = {
24
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
25
+ pageId: integer("pageId")
26
+ .notNull()
27
+ .references(() => linkPages.id, { onDelete: "cascade" }),
28
+ title: text("title").notNull(),
29
+ url: text("url").notNull(),
30
+ position: integer("position").notNull().default(0),
31
+ isVisible: boolean("isVisible").notNull().default(true),
32
+ createdAt: timestamp("createdAt", { mode: "date", precision: 3 }).notNull().defaultNow(),
33
+ updatedAt: timestamp("updatedAt", { mode: "date", precision: 3 })
34
+ .notNull()
35
+ .$defaultFn(() => new Date())
36
+ .$onUpdateFn(() => new Date()),
37
+ } as const;
38
+
39
+ export const linkPageItems = pgTable("link_page_items", linkPageItemColumns, (t) => [
40
+ index("link_page_items_pageId_idx").on(t.pageId),
41
+ ]);
@@ -0,0 +1,12 @@
1
+ import { relations } from "drizzle-orm";
2
+ import { releases } from "@gigamusic/db/schema";
3
+ import { linkPages, linkPageItems } from "./link-pages.js";
4
+
5
+ export const linkPagesRelations = relations(linkPages, ({ one, many }) => ({
6
+ release: one(releases, { fields: [linkPages.releaseId], references: [releases.id] }),
7
+ items: many(linkPageItems),
8
+ }));
9
+
10
+ export const linkPageItemsRelations = relations(linkPageItems, ({ one }) => ({
11
+ page: one(linkPages, { fields: [linkPageItems.pageId], references: [linkPages.id] }),
12
+ }));
package/src/server.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  // because the handlers depend on `next/cache`.
4
4
 
5
5
  export { createLinkPageQueries } from "./queries/link-pages";
6
+ export type { LinkPagesDb } from "./queries/link-pages";
6
7
  export {
7
8
  createAdminLinkPagesHandlers,
8
9
  createAdminLinkPageByIdHandlers,
@@ -13,10 +14,3 @@ export type {
13
14
  LinkPagesAdminDeps,
14
15
  RouteHandler,
15
16
  } from "./api/handlers";
16
- export {
17
- PublicLinkPage,
18
- createPublicLinkPage,
19
- createPublicLinkPageMetadata,
20
- publicLinkPageMetadata,
21
- registerLinkPageMetadata,
22
- } from "./pages/PublicLinkPage";
package/src/types.ts CHANGED
@@ -1,18 +1,10 @@
1
1
  import type { LinkPageInput, LinkPageItemInput } from "@gigamusic/core";
2
+ import type { linkPageItems, linkPages } from "./schema/index.js";
2
3
 
3
4
  export type { LinkPageInput, LinkPageItemInput };
4
5
 
5
6
  /** A single link button rendered inside a `LinkPage`. */
6
- export interface LinkPageItem {
7
- id: number;
8
- pageId: number;
9
- title: string;
10
- url: string;
11
- position: number;
12
- isVisible: boolean;
13
- createdAt: Date;
14
- updatedAt: Date;
15
- }
7
+ export type LinkPageItem = typeof linkPageItems.$inferSelect;
16
8
 
17
9
  /** Optional `Release` reference exposed to the public view for cover fallback. */
18
10
  export interface LinkPageReleaseRef {
@@ -23,18 +15,8 @@ export interface LinkPageReleaseRef {
23
15
  isPublished: boolean;
24
16
  }
25
17
 
26
- /** Top-level "Linktree-style" page row. IDs are native `Int @id @default(autoincrement())`. */
27
- export interface LinkPage {
28
- id: number;
29
- slug: string;
30
- title: string;
31
- description: string | null;
32
- coverImageUrl: string | null;
33
- releaseId: number | null;
34
- isPublished: boolean;
35
- createdAt: Date;
36
- updatedAt: Date;
37
- }
18
+ /** Top-level "Linktree-style" page row. */
19
+ export type LinkPage = typeof linkPages.$inferSelect;
38
20
 
39
21
  /** A `LinkPage` joined with its visible (or all) items and an optional release ref. */
40
22
  export interface LinkPageWithItems extends LinkPage {
@@ -43,7 +25,7 @@ export interface LinkPageWithItems extends LinkPage {
43
25
  }
44
26
 
45
27
  /**
46
- * Query helpers that close over a `PrismaClient`. Consumers merge this with
28
+ * Query helpers that close over a Drizzle DB. Consumers merge this with
47
29
  * `@gigamusic/db`'s `Queries` factory result (or pass it through wherever a
48
30
  * `LinkPageQueries` is needed).
49
31
  */