@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 +49 -17
- package/package.json +16 -31
- package/src/index.ts +3 -52
- package/src/platforms/detect.ts +10 -3
- package/src/queries/link-pages.ts +72 -77
- package/src/schema/index.ts +2 -0
- package/src/schema/link-pages.ts +41 -0
- package/src/schema/relations.ts +12 -0
- package/src/server.ts +1 -7
- package/src/types.ts +5 -23
- package/dist/cli.js +0 -191
- package/dist/cli.js.map +0 -1
- package/prisma/_typegen.prisma +0 -29
- package/prisma/link-page.prisma +0 -54
- package/src/cli/index.ts +0 -95
- package/src/cli/paths.ts +0 -11
- package/src/cli/sync.ts +0 -169
- package/src/client.ts +0 -10
- package/src/components/LinkButton.tsx +0 -20
- package/src/components/LinkPageHeader.tsx +0 -35
- package/src/components/LinkPageView.tsx +0 -64
- package/src/components/admin/DeleteLinkPageButton.tsx +0 -52
- package/src/components/admin/LinkPageForm.tsx +0 -490
- package/src/components/admin/NewLinkPageForm.tsx +0 -152
- package/src/components/url-helpers.ts +0 -18
- package/src/pages/AdminEditLinkPagePage.tsx +0 -57
- package/src/pages/AdminLinkPagesIndexPage.tsx +0 -107
- package/src/pages/AdminNewLinkPagePage.tsx +0 -26
- package/src/pages/PublicLinkPage.tsx +0 -117
- package/src/slots-context.tsx +0 -46
- package/src/slots.ts +0 -36
package/README.md
CHANGED
|
@@ -1,34 +1,66 @@
|
|
|
1
1
|
# @gigamusic/links
|
|
2
2
|
|
|
3
|
-
Linktree-style per-release link pages for
|
|
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
|
|
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
|
-
##
|
|
24
|
+
## Schema subpath
|
|
21
25
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Editable Linktree-style link pages for gigamusic artist sites. Ships a
|
|
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/
|
|
42
|
-
"@gigamusic/
|
|
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
|
-
"
|
|
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
|
-
"
|
|
59
|
-
"
|
|
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": "
|
|
72
|
-
"typecheck": "
|
|
73
|
-
"
|
|
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
|
-
//
|
|
2
|
-
//
|
|
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,
|
package/src/platforms/detect.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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:
|
|
28
|
+
export function createLinkPageQueries(db: LinkPagesDb): LinkPageQueries {
|
|
19
29
|
return {
|
|
20
30
|
listPublishedLinkPages: async (): Promise<LinkPage[]> => {
|
|
21
|
-
const rows = await db.
|
|
22
|
-
where:
|
|
23
|
-
orderBy:
|
|
31
|
+
const rows = await db.query.linkPages.findMany({
|
|
32
|
+
where: eq(linkPages.isPublished, true),
|
|
33
|
+
orderBy: desc(linkPages.updatedAt),
|
|
24
34
|
});
|
|
25
|
-
return rows
|
|
35
|
+
return rows;
|
|
26
36
|
},
|
|
27
37
|
|
|
28
38
|
listAllLinkPages: async (): Promise<LinkPage[]> => {
|
|
29
|
-
const rows = await db.
|
|
30
|
-
orderBy:
|
|
39
|
+
const rows = await db.query.linkPages.findMany({
|
|
40
|
+
orderBy: desc(linkPages.updatedAt),
|
|
31
41
|
});
|
|
32
|
-
return rows
|
|
42
|
+
return rows;
|
|
33
43
|
},
|
|
34
44
|
|
|
35
|
-
getPublicLinkPageBySlug: async (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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:
|
|
52
|
-
orderBy:
|
|
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.
|
|
61
|
-
where:
|
|
62
|
-
|
|
68
|
+
const row = await db.query.linkPages.findFirst({
|
|
69
|
+
where: eq(linkPages.id, id),
|
|
70
|
+
with: {
|
|
63
71
|
release: {
|
|
64
|
-
|
|
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:
|
|
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
|
|
80
|
-
|
|
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
|
|
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.
|
|
105
|
-
|
|
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.
|
|
114
|
+
await db.delete(linkPages).where(eq(linkPages.id, id));
|
|
113
115
|
},
|
|
114
116
|
|
|
115
|
-
addLinkPageItem: async (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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:
|
|
129
|
+
position: nextPosition,
|
|
129
130
|
isVisible: input.isVisible ?? true,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return row
|
|
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.
|
|
145
|
-
|
|
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.
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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,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
|
|
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.
|
|
27
|
-
export
|
|
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
|
|
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
|
*/
|