@gigamusic/links 1.0.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,6 +1,6 @@
1
1
  # @gigamusic/links
2
2
 
3
- Linktree-style per-release link pages for the gigamusic platform. Ships its own Prisma fragment (`LinkPage`, `LinkPageItem`), a typed queries factory, admin API handler factories, platform-detection helpers, and a small SVG `LinkPlatformIcon` component.
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.
4
4
 
5
5
  ```ts
6
6
  import {
@@ -19,18 +19,48 @@ import {
19
19
  } from "@gigamusic/links/server";
20
20
  ```
21
21
 
22
- The public link-page UI is the consumer's responsibility — fetch with `createLinkPageQueries(prisma).getPublicLinkPageBySlug(slug)` and render whatever you want.
22
+ The public link-page UI is the consumer's responsibility — fetch with `createLinkPageQueries(db).getPublicLinkPageBySlug(slug)` and render whatever you want.
23
23
 
24
- ## Prisma fragment
24
+ ## Schema subpath
25
25
 
26
- The `link-page.prisma` fragment ships under `node_modules/@gigamusic/links/prisma/`. Copy it into your `prisma/schema/` directory (Prisma 6 `prismaSchemaFolder` mode) once per upgrade.
26
+ `@gigamusic/links/schema` exports the table objects and relations:
27
27
 
28
- 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`:
28
+ ```ts
29
+ import {
30
+ linkPages,
31
+ linkPageItems,
32
+ linkPagesRelations,
33
+ linkPageItemsRelations,
34
+ linkPageColumns,
35
+ linkPageItemColumns,
36
+ } from "@gigamusic/links/schema";
37
+ ```
38
+
39
+ Merge with `@gigamusic/db/schema` when constructing your Drizzle instance:
29
40
 
30
- ```prisma
31
- // prisma/schema/99-app.prisma (or wherever your Release lives)
32
- model Release {
33
- // ...existing fields...
34
- linkPages LinkPage[]
35
- }
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 });
36
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";
59
+
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
+ }));
64
+ ```
65
+
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": "1.0.0",
4
- "description": "Editable Linktree-style link pages for gigamusic artist sites. Ships a Prisma fragment, 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",
@@ -17,11 +17,14 @@
17
17
  "./server": {
18
18
  "types": "./src/server.ts",
19
19
  "default": "./src/server.ts"
20
+ },
21
+ "./schema": {
22
+ "types": "./src/schema/index.ts",
23
+ "default": "./src/schema/index.ts"
20
24
  }
21
25
  },
22
26
  "files": [
23
27
  "src",
24
- "prisma",
25
28
  "README.md"
26
29
  ],
27
30
  "publishConfig": {
@@ -29,29 +32,30 @@
29
32
  },
30
33
  "dependencies": {
31
34
  "@gigamusic/core": "1.0.0",
32
- "@gigamusic/db": "1.0.0"
35
+ "@gigamusic/db": "2.0.0"
33
36
  },
34
37
  "peerDependencies": {
35
- "@prisma/client": ">=6",
38
+ "drizzle-orm": ">=0.45",
36
39
  "next": ">=15",
37
40
  "react": ">=18 <20",
38
41
  "react-dom": ">=18 <20"
39
42
  },
40
43
  "devDependencies": {
41
- "@prisma/client": "^6.1.0",
42
44
  "@types/node": "^22.10.5",
45
+ "@types/pg": "^8.11.10",
43
46
  "@types/react": "^19.0.0",
47
+ "drizzle-kit": "^0.31.10",
48
+ "drizzle-orm": "^0.45.2",
44
49
  "next": "^16.0.0",
45
- "prisma": "^6.1.0",
50
+ "pg": "^8.21.0",
46
51
  "react": "^19.0.0",
47
52
  "typescript": "^5.7.3",
48
53
  "vitest": "^2.1.8"
49
54
  },
50
55
  "scripts": {
51
56
  "lint": "eslint src",
52
- "test": "pnpm run prisma:generate && vitest run",
53
- "typecheck": "pnpm run prisma:generate && tsc --noEmit",
54
- "prisma:generate": "prisma generate --schema=./prisma",
57
+ "test": "vitest run",
58
+ "typecheck": "tsc --noEmit",
55
59
  "clean": "rm -rf .turbo *.tsbuildinfo"
56
60
  }
57
61
  }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ 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";
@@ -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,
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
  */
@@ -1,29 +0,0 @@
1
- // Internal — exists ONLY so `prisma generate --schema=./prisma` in THIS
2
- // package can produce a PrismaClient with LinkPage + LinkPageItem on the
3
- // delegate. The `gigamusic-links sync` CLI skips files starting with `_`,
4
- // so this is never copied into a consumer app. The consumer's merged
5
- // schema gets `Release` from `@gigamusic/db`; here we declare a minimal
6
- // stub so the link-page.prisma relation compiles in isolation.
7
-
8
- datasource db {
9
- provider = "postgresql"
10
- url = env("DATABASE_URL")
11
- }
12
-
13
- generator client {
14
- provider = "prisma-client-js"
15
- binaryTargets = ["native"]
16
- }
17
-
18
- /// Minimal stub of @gigamusic/db's `Release` model — only the fields this
19
- /// package's queries select. The consumer's merged schema redefines `Release`
20
- /// with all its fields; this stub only needs to satisfy the link-page relation
21
- /// and the columns we read.
22
- model Release {
23
- id Int @id @default(autoincrement())
24
- name String
25
- slug String @unique
26
- coverImageUrl String?
27
- isPublished Boolean @default(false)
28
- linkPages LinkPage[]
29
- }
@@ -1,54 +0,0 @@
1
- // gigamusic link-page schema fragment
2
- //
3
- // Shipped by `@gigamusic/links` and copied into a consumer app's
4
- // `prisma/schema/` directory by `gigamusic-links sync` with a `20-` prefix
5
- // (after `10-` fragments from @gigamusic/db, before consumer-owned `99-`).
6
- //
7
- // `LinkPage` references the `Release` model from @gigamusic/db via the
8
- // `releaseId` scalar foreign key. Prisma requires *both* sides of every
9
- // relation to be declared, so for the `release Release?` relation to compile
10
- // the consumer must extend the upstream `Release` model with:
11
- //
12
- // model Release {
13
- // // ...existing fields...
14
- // linkPages LinkPage[]
15
- // }
16
- //
17
- // The demo + create-gigamusic-app scaffolding wire this up automatically.
18
- //
19
- // Reserved model names: LinkPage, LinkPageItem
20
- // Reserved table names: link_pages, link_page_items
21
-
22
- model LinkPage {
23
- id Int @id @default(autoincrement())
24
- slug String @unique
25
- title String
26
- description String?
27
- coverImageUrl String?
28
- releaseId Int?
29
- isPublished Boolean @default(true)
30
- createdAt DateTime @default(now())
31
- updatedAt DateTime @updatedAt
32
-
33
- release Release? @relation(fields: [releaseId], references: [id], onDelete: SetNull)
34
- items LinkPageItem[]
35
-
36
- @@index([releaseId])
37
- @@map("link_pages")
38
- }
39
-
40
- model LinkPageItem {
41
- id Int @id @default(autoincrement())
42
- pageId Int
43
- title String
44
- url String
45
- position Int @default(0)
46
- isVisible Boolean @default(true)
47
- createdAt DateTime @default(now())
48
- updatedAt DateTime @updatedAt
49
-
50
- page LinkPage @relation(fields: [pageId], references: [id], onDelete: Cascade)
51
-
52
- @@index([pageId])
53
- @@map("link_page_items")
54
- }