@gigamusic/links 3.0.0 → 4.2.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 +16 -25
- package/package.json +4 -13
- package/src/api/handlers.ts +8 -13
- package/src/index.ts +5 -11
- package/src/server.ts +3 -5
- package/src/queries/link-pages.ts +0 -211
- package/src/schema/index.ts +0 -2
- package/src/schema/link-pages.ts +0 -56
- package/src/schema/relations.ts +0 -54
- package/src/types.ts +0 -76
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# @gigamusic/links
|
|
2
2
|
|
|
3
|
-
Linktree-style per-release link pages for the gigamusic platform. Ships
|
|
3
|
+
Linktree-style per-release link pages for the gigamusic platform. Ships admin API handler factories, platform-detection helpers, and a small SVG `LinkPlatformIcon` component.
|
|
4
|
+
|
|
5
|
+
The `link_pages` / `link_page_items` Drizzle schema **and** the link-page queries live in `@gigamusic/db` — they're folded into the single `createQueries(db)` so a consumer builds one query object for every table. This package re-exports the link-page row types for convenience.
|
|
4
6
|
|
|
5
7
|
```ts
|
|
6
8
|
import {
|
|
7
|
-
createLinkPageQueries,
|
|
8
9
|
detectLinkPlatform,
|
|
9
10
|
PLATFORM_LABELS,
|
|
10
11
|
LinkPlatformIcon,
|
|
11
12
|
type KnownLinkPlatform,
|
|
13
|
+
type LinkPageWithItems,
|
|
12
14
|
} from "@gigamusic/links";
|
|
13
15
|
|
|
14
16
|
// Admin handler factories live on the /server subpath
|
|
@@ -19,43 +21,32 @@ import {
|
|
|
19
21
|
} from "@gigamusic/links/server";
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
## Schema subpath
|
|
24
|
+
## Reads and writes
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
All link-page DB access comes from `@gigamusic/db`'s queries:
|
|
27
27
|
|
|
28
28
|
```ts
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
linkPageItemColumns,
|
|
36
|
-
} from "@gigamusic/links/schema";
|
|
29
|
+
import { createQueries } from "@gigamusic/db";
|
|
30
|
+
|
|
31
|
+
const queries = createQueries(db); // one object, every table
|
|
32
|
+
|
|
33
|
+
// Public link-page UI — fetch and render whatever you want:
|
|
34
|
+
const page = await queries.getPublicLinkPageBySlug(slug);
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
The admin handler factories from `@gigamusic/links/server` take that same `queries` object:
|
|
40
38
|
|
|
41
39
|
```ts
|
|
42
|
-
|
|
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 });
|
|
40
|
+
const handlers = createAdminLinkPagesHandlers({ queries });
|
|
49
41
|
```
|
|
50
42
|
|
|
51
43
|
## Release back-reference
|
|
52
44
|
|
|
53
|
-
`linkPages.releaseId` is a nullable FK to `releases.id
|
|
45
|
+
`linkPages.releaseId` is a nullable FK to `releases.id`. Both tables and their relations live in `@gigamusic/db/schema`; `linkPagesRelations` already declares the `linkPage.release` direction. If you want a `release.linkPages` back-reference (for `db.query.releases.findFirst({ with: { linkPages: true } })`), compose it onto the base `releasesRelations` in your own schema file:
|
|
54
46
|
|
|
55
47
|
```ts
|
|
56
48
|
import { relations } from "drizzle-orm";
|
|
57
|
-
import { releases } from "@gigamusic/db/schema";
|
|
58
|
-
import { linkPages } from "@gigamusic/links/schema";
|
|
49
|
+
import { releases, linkPages } from "@gigamusic/db/schema";
|
|
59
50
|
|
|
60
51
|
export const releasesRelations = relations(releases, ({ many }) => ({
|
|
61
52
|
// ...keep the existing many() relations from @gigamusic/db/schema if you need them...
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gigamusic/links",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Editable Linktree-style link pages for gigamusic artist sites. Ships
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"description": "Editable Linktree-style link pages for gigamusic artist sites. Ships admin API handler factories and platform-detection helpers (the Drizzle schema + queries live in @gigamusic/db).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -17,10 +17,6 @@
|
|
|
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"
|
|
24
20
|
}
|
|
25
21
|
},
|
|
26
22
|
"files": [
|
|
@@ -32,23 +28,18 @@
|
|
|
32
28
|
},
|
|
33
29
|
"dependencies": {
|
|
34
30
|
"zod": "^3.24.1",
|
|
35
|
-
"@gigamusic/db": "
|
|
36
|
-
"@gigamusic/core": "
|
|
31
|
+
"@gigamusic/db": "4.2.0",
|
|
32
|
+
"@gigamusic/core": "4.2.0"
|
|
37
33
|
},
|
|
38
34
|
"peerDependencies": {
|
|
39
|
-
"drizzle-orm": ">=0.45",
|
|
40
35
|
"next": ">=15",
|
|
41
36
|
"react": ">=18 <20",
|
|
42
37
|
"react-dom": ">=18 <20"
|
|
43
38
|
},
|
|
44
39
|
"devDependencies": {
|
|
45
40
|
"@types/node": "^22.10.5",
|
|
46
|
-
"@types/pg": "^8.11.10",
|
|
47
41
|
"@types/react": "^19.0.0",
|
|
48
|
-
"drizzle-kit": "^0.31.10",
|
|
49
|
-
"drizzle-orm": "^0.45.2",
|
|
50
42
|
"next": "^16.0.0",
|
|
51
|
-
"pg": "^8.21.0",
|
|
52
43
|
"react": "^19.0.0",
|
|
53
44
|
"typescript": "^5.7.3",
|
|
54
45
|
"vitest": "^2.1.8"
|
package/src/api/handlers.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { revalidateTag } from "next/cache";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { LinkPageInputSchema, LinkPageItemInputSchema } from "@gigamusic/core";
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
LinkPageInput,
|
|
7
|
-
LinkPageItemInput,
|
|
8
|
-
LinkPageQueries,
|
|
9
|
-
LinkPageQueryTables,
|
|
10
|
-
} from "../types";
|
|
4
|
+
import type { LinkPageInput, LinkPageItemInput } from "@gigamusic/core";
|
|
5
|
+
import type { DefaultTables, Queries, QueryTables } from "@gigamusic/db";
|
|
11
6
|
import {
|
|
12
7
|
INVALID_SLUG_MESSAGE,
|
|
13
8
|
RESERVED_SLUGS,
|
|
@@ -32,7 +27,7 @@ export type RouteHandler<Params = Record<string, string>> = (
|
|
|
32
27
|
/**
|
|
33
28
|
* Deps for the admin-side link-page handler factories.
|
|
34
29
|
*
|
|
35
|
-
* Generic over the consumer's `
|
|
30
|
+
* Generic over the consumer's `QueryTables` so the `Queries` and the
|
|
36
31
|
* `afterWrite` callbacks see consumer-extended `linkPages` / `linkPageItems`
|
|
37
32
|
* row shapes when the consumer built tables via `buildLinkPages(...)` /
|
|
38
33
|
* `buildLinkPageItems(...)`.
|
|
@@ -41,11 +36,11 @@ export type RouteHandler<Params = Record<string, string>> = (
|
|
|
41
36
|
* `proxy.ts` / middleware. These handlers don't verify a session.
|
|
42
37
|
*/
|
|
43
38
|
export interface LinkPagesAdminDeps<
|
|
44
|
-
T extends
|
|
39
|
+
T extends QueryTables = DefaultTables,
|
|
45
40
|
TPageInput extends z.ZodObject<any> = typeof LinkPageInputSchema,
|
|
46
41
|
TItemInput extends z.ZodObject<any> = typeof LinkPageItemInputSchema,
|
|
47
42
|
> {
|
|
48
|
-
queries:
|
|
43
|
+
queries: Queries<T>;
|
|
49
44
|
/** Defaults to `LinkPageInputSchema`. Pass an `.extend()`ed schema for extras. */
|
|
50
45
|
pageInputSchema?: TPageInput;
|
|
51
46
|
/** Defaults to `LinkPageItemInputSchema`. Pass an `.extend()`ed schema for extras. */
|
|
@@ -80,7 +75,7 @@ function validateSlug(slug: string): { ok: true } | { ok: false; res: Response }
|
|
|
80
75
|
* `app/api/admin/link-pages/route.ts` handlers — list + create.
|
|
81
76
|
*/
|
|
82
77
|
export function createAdminLinkPagesHandlers<
|
|
83
|
-
T extends
|
|
78
|
+
T extends QueryTables = DefaultTables,
|
|
84
79
|
TPageInput extends z.ZodObject<any> = typeof LinkPageInputSchema,
|
|
85
80
|
TItemInput extends z.ZodObject<any> = typeof LinkPageItemInputSchema,
|
|
86
81
|
>(deps: LinkPagesAdminDeps<T, TPageInput, TItemInput>): {
|
|
@@ -139,7 +134,7 @@ export function createAdminLinkPagesHandlers<
|
|
|
139
134
|
* `app/api/admin/link-pages/[id]/route.ts` handlers — single-page CRUD.
|
|
140
135
|
*/
|
|
141
136
|
export function createAdminLinkPageByIdHandlers<
|
|
142
|
-
T extends
|
|
137
|
+
T extends QueryTables = DefaultTables,
|
|
143
138
|
TPageInput extends z.ZodObject<any> = typeof LinkPageInputSchema,
|
|
144
139
|
TItemInput extends z.ZodObject<any> = typeof LinkPageItemInputSchema,
|
|
145
140
|
>(deps: LinkPagesAdminDeps<T, TPageInput, TItemInput>): {
|
|
@@ -229,7 +224,7 @@ interface LinkPageItemBody {
|
|
|
229
224
|
* DELETE removes an item by `itemId` in the query string.
|
|
230
225
|
*/
|
|
231
226
|
export function createAdminLinkPageItemsHandlers<
|
|
232
|
-
T extends
|
|
227
|
+
T extends QueryTables = DefaultTables,
|
|
233
228
|
TPageInput extends z.ZodObject<any> = typeof LinkPageInputSchema,
|
|
234
229
|
TItemInput extends z.ZodObject<any> = typeof LinkPageItemInputSchema,
|
|
235
230
|
>(deps: LinkPagesAdminDeps<T, TPageInput, TItemInput>): {
|
package/src/index.ts
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
// Public entry —
|
|
2
|
-
//
|
|
1
|
+
// Public entry — platform helpers + UI, plus a convenience re-export of the
|
|
2
|
+
// link-page DB types. The queries themselves come from `@gigamusic/db`'s
|
|
3
|
+
// `createQueries(db, site)`; admin handler factories live on `./server`.
|
|
3
4
|
|
|
4
|
-
// ── Types
|
|
5
|
+
// ── Types (re-exported from @gigamusic/db, the single source for DB types) ─
|
|
5
6
|
export type {
|
|
6
|
-
DefaultLinkPageTables,
|
|
7
7
|
LinkPage,
|
|
8
8
|
LinkPageItem,
|
|
9
9
|
LinkPageReleaseRef,
|
|
10
10
|
LinkPageWithItems,
|
|
11
|
-
LinkPageQueries,
|
|
12
|
-
LinkPageQueryTables,
|
|
13
11
|
LinkPageInput,
|
|
14
12
|
LinkPageItemInput,
|
|
15
|
-
} from "
|
|
16
|
-
|
|
17
|
-
// ── Queries factory ─────────────────────────────────────────────────────
|
|
18
|
-
export { createLinkPageQueries } from "./queries/link-pages";
|
|
19
|
-
export type { LinkPagesDb } from "./queries/link-pages";
|
|
13
|
+
} from "@gigamusic/db";
|
|
20
14
|
|
|
21
15
|
// ── Platform detection ──────────────────────────────────────────────────
|
|
22
16
|
export { detectLinkPlatform, PLATFORM_LABELS } from "./platforms/detect";
|
package/src/server.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
// Server-only entry — admin API handler factories
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Server-only entry — admin API handler factories. Importing from here in a
|
|
2
|
+
// `"use client"` file will cause a build error because the handlers depend on
|
|
3
|
+
// `next/cache`. (The query factory lives in `@gigamusic/db`'s `createQueries`.)
|
|
4
4
|
|
|
5
|
-
export { createLinkPageQueries } from "./queries/link-pages";
|
|
6
|
-
export type { LinkPagesDb } from "./queries/link-pages";
|
|
7
5
|
export {
|
|
8
6
|
createAdminLinkPagesHandlers,
|
|
9
7
|
createAdminLinkPageByIdHandlers,
|
|
@@ -1,211 +0,0 @@
|
|
|
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";
|
|
5
|
-
import {
|
|
6
|
-
linkPageItems as baseLinkPageItems,
|
|
7
|
-
linkPages as baseLinkPages,
|
|
8
|
-
} from "../schema/index";
|
|
9
|
-
import type {
|
|
10
|
-
DefaultLinkPageTables,
|
|
11
|
-
LinkPage,
|
|
12
|
-
LinkPageInput,
|
|
13
|
-
LinkPageItem,
|
|
14
|
-
LinkPageItemInput,
|
|
15
|
-
LinkPageQueries,
|
|
16
|
-
LinkPageQueryTables,
|
|
17
|
-
LinkPageWithItems,
|
|
18
|
-
} from "../types";
|
|
19
|
-
|
|
20
|
-
type RequiredSchema = typeof gigamusicDbSchema & typeof linksSchema;
|
|
21
|
-
|
|
22
|
-
/** Drizzle DB type the link-pages factory needs — a superset that includes both link-pages tables and `releases`. */
|
|
23
|
-
export type LinkPagesDb = NodePgDatabase<RequiredSchema>;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Factory that binds a Drizzle database into the `LinkPageQueries` contract.
|
|
27
|
-
* Consumers typically merge the result with `@gigamusic/db`'s `createQueries`.
|
|
28
|
-
*
|
|
29
|
-
* Pass a `tables` arg to thread consumer-extended `linkPages` / `linkPageItems`
|
|
30
|
-
* (built via the `buildLinkPages` / `buildLinkPageItems` factories) through
|
|
31
|
-
* every query — the returned `LinkPageQueries<T>` rows carry the consumer's
|
|
32
|
-
* extra columns. Single-arg callers fall back to the un-extended convenience
|
|
33
|
-
* tables.
|
|
34
|
-
*
|
|
35
|
-
* The `db` instance must have been built with a schema that includes the
|
|
36
|
-
* union of `@gigamusic/db/schema` and `@gigamusic/links/schema`; otherwise
|
|
37
|
-
* the relational `with: { release: ... }` clause won't resolve at runtime.
|
|
38
|
-
*/
|
|
39
|
-
export function createLinkPageQueries(db: LinkPagesDb): LinkPageQueries;
|
|
40
|
-
export function createLinkPageQueries<T extends LinkPageQueryTables>(
|
|
41
|
-
db: LinkPagesDb,
|
|
42
|
-
tables: T,
|
|
43
|
-
): LinkPageQueries<T>;
|
|
44
|
-
export function createLinkPageQueries<T extends LinkPageQueryTables>(
|
|
45
|
-
db: LinkPagesDb,
|
|
46
|
-
tables?: T,
|
|
47
|
-
): LinkPageQueries<T> {
|
|
48
|
-
// Identity is preserved at runtime; the casts only widen for the type
|
|
49
|
-
// checker so consumer-extended tables don't have to match the base shape
|
|
50
|
-
// exactly.
|
|
51
|
-
const linkPages = (tables?.linkPages ?? baseLinkPages) as unknown as typeof baseLinkPages;
|
|
52
|
-
const linkPageItems = (tables?.linkPageItems ??
|
|
53
|
-
baseLinkPageItems) as unknown as typeof baseLinkPageItems;
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
listPublishedLinkPages: async (): Promise<LinkPage<T>[]> => {
|
|
57
|
-
const rows = await db.query.linkPages.findMany({
|
|
58
|
-
where: eq(linkPages.isPublished, true),
|
|
59
|
-
orderBy: desc(linkPages.updatedAt),
|
|
60
|
-
});
|
|
61
|
-
return rows as unknown as LinkPage<T>[];
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
listAllLinkPages: async (): Promise<LinkPage<T>[]> => {
|
|
65
|
-
const rows = await db.query.linkPages.findMany({
|
|
66
|
-
orderBy: desc(linkPages.updatedAt),
|
|
67
|
-
});
|
|
68
|
-
return rows as unknown as LinkPage<T>[];
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
getPublicLinkPageBySlug: async (slug: string): Promise<LinkPageWithItems<T> | null> => {
|
|
72
|
-
const row = await db.query.linkPages.findFirst({
|
|
73
|
-
where: and(eq(linkPages.slug, slug), eq(linkPages.isPublished, true)),
|
|
74
|
-
with: {
|
|
75
|
-
release: {
|
|
76
|
-
columns: {
|
|
77
|
-
id: true,
|
|
78
|
-
name: true,
|
|
79
|
-
slug: true,
|
|
80
|
-
coverImageUrl: true,
|
|
81
|
-
isPublished: true,
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
items: {
|
|
85
|
-
where: eq(linkPageItems.isVisible, true),
|
|
86
|
-
orderBy: asc(linkPageItems.position),
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
return (row ?? null) as unknown as LinkPageWithItems<T> | null;
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
getLinkPageById: async (id: number): Promise<LinkPageWithItems<T> | null> => {
|
|
94
|
-
const row = await db.query.linkPages.findFirst({
|
|
95
|
-
where: eq(linkPages.id, id),
|
|
96
|
-
with: {
|
|
97
|
-
release: {
|
|
98
|
-
columns: {
|
|
99
|
-
id: true,
|
|
100
|
-
name: true,
|
|
101
|
-
slug: true,
|
|
102
|
-
coverImageUrl: true,
|
|
103
|
-
isPublished: true,
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
items: { orderBy: asc(linkPageItems.position) },
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
return (row ?? null) as unknown as LinkPageWithItems<T> | null;
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
createLinkPage: async (input: LinkPageInput): Promise<LinkPage<T>> => {
|
|
113
|
-
const [row] = await db
|
|
114
|
-
.insert(linkPages)
|
|
115
|
-
.values({
|
|
116
|
-
title: input.title,
|
|
117
|
-
slug: input.slug,
|
|
118
|
-
description: input.description ?? null,
|
|
119
|
-
releaseId: input.releaseId ?? null,
|
|
120
|
-
coverImageUrl: input.coverImageUrl ?? null,
|
|
121
|
-
isPublished: input.isPublished ?? true,
|
|
122
|
-
})
|
|
123
|
-
.returning();
|
|
124
|
-
return row! as unknown as LinkPage<T>;
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
updateLinkPage: async (id: number, input: Partial<LinkPageInput>): Promise<LinkPage<T>> => {
|
|
128
|
-
const data: Record<string, unknown> = {};
|
|
129
|
-
if (input.title !== undefined) data.title = input.title;
|
|
130
|
-
if (input.slug !== undefined) data.slug = input.slug;
|
|
131
|
-
if (input.description !== undefined) data.description = input.description;
|
|
132
|
-
if (input.releaseId !== undefined) data.releaseId = input.releaseId;
|
|
133
|
-
if (input.coverImageUrl !== undefined) data.coverImageUrl = input.coverImageUrl;
|
|
134
|
-
if (input.isPublished !== undefined) data.isPublished = input.isPublished;
|
|
135
|
-
const [row] = await db.update(linkPages).set(data).where(eq(linkPages.id, id)).returning();
|
|
136
|
-
return row! as unknown as LinkPage<T>;
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
deleteLinkPage: async (id: number): Promise<void> => {
|
|
140
|
-
await db.delete(linkPages).where(eq(linkPages.id, id));
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
addLinkPageItem: async (
|
|
144
|
-
pageId: number,
|
|
145
|
-
input: LinkPageItemInput,
|
|
146
|
-
): Promise<LinkPageItem<T>> => {
|
|
147
|
-
const [maxRow] = await db
|
|
148
|
-
.select({ max: max(linkPageItems.position) })
|
|
149
|
-
.from(linkPageItems)
|
|
150
|
-
.where(eq(linkPageItems.pageId, pageId));
|
|
151
|
-
const nextPosition = input.position ?? ((maxRow?.max ?? -1) + 1);
|
|
152
|
-
const [row] = await db
|
|
153
|
-
.insert(linkPageItems)
|
|
154
|
-
.values({
|
|
155
|
-
pageId,
|
|
156
|
-
title: input.title,
|
|
157
|
-
url: input.url,
|
|
158
|
-
position: nextPosition,
|
|
159
|
-
isVisible: input.isVisible ?? true,
|
|
160
|
-
})
|
|
161
|
-
.returning();
|
|
162
|
-
return row! as unknown as LinkPageItem<T>;
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
updateLinkPageItem: async (
|
|
166
|
-
id: number,
|
|
167
|
-
input: Partial<LinkPageItemInput>,
|
|
168
|
-
): Promise<LinkPageItem<T>> => {
|
|
169
|
-
const data: Record<string, unknown> = {};
|
|
170
|
-
if (input.title !== undefined) data.title = input.title;
|
|
171
|
-
if (input.url !== undefined) data.url = input.url;
|
|
172
|
-
if (input.position !== undefined) data.position = input.position;
|
|
173
|
-
if (input.isVisible !== undefined) data.isVisible = input.isVisible;
|
|
174
|
-
const [row] = await db
|
|
175
|
-
.update(linkPageItems)
|
|
176
|
-
.set(data)
|
|
177
|
-
.where(eq(linkPageItems.id, id))
|
|
178
|
-
.returning();
|
|
179
|
-
return row! as unknown as LinkPageItem<T>;
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
deleteLinkPageItem: async (id: number): Promise<void> => {
|
|
183
|
-
await db.delete(linkPageItems).where(eq(linkPageItems.id, id));
|
|
184
|
-
},
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Rewrite every item's `position` to match the caller's preferred order
|
|
188
|
-
* inside a single transaction. IDs not present in `orderedItemIds` keep
|
|
189
|
-
* their current position — callers must include the full set to fully
|
|
190
|
-
* resort.
|
|
191
|
-
*/
|
|
192
|
-
reorderLinkPageItems: async (pageId: number, orderedItemIds: number[]): Promise<void> => {
|
|
193
|
-
await db.transaction(async (tx) => {
|
|
194
|
-
for (let position = 0; position < orderedItemIds.length; position++) {
|
|
195
|
-
await tx
|
|
196
|
-
.update(linkPageItems)
|
|
197
|
-
.set({ position })
|
|
198
|
-
.where(
|
|
199
|
-
and(
|
|
200
|
-
eq(linkPageItems.id, orderedItemIds[position]!),
|
|
201
|
-
eq(linkPageItems.pageId, pageId),
|
|
202
|
-
),
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Re-export DefaultLinkPageTables so the consumer can import it if they want.
|
|
211
|
-
export type { DefaultLinkPageTables };
|
package/src/schema/index.ts
DELETED
package/src/schema/link-pages.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { boolean, index, integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
-
import { releases } from "@gigamusic/db/schema";
|
|
3
|
-
import type { ExtraColumns } from "@gigamusic/db/schema";
|
|
4
|
-
|
|
5
|
-
// See @gigamusic/db/schema/releases.ts for the rationale on inlining
|
|
6
|
-
// columns inside the factory (TTableName generic resolution).
|
|
7
|
-
export function buildLinkPages<TExtras extends ExtraColumns = {}>(
|
|
8
|
-
extras: TExtras = {} as TExtras,
|
|
9
|
-
) {
|
|
10
|
-
return pgTable(
|
|
11
|
-
"link_pages",
|
|
12
|
-
{
|
|
13
|
-
id: serial("id").primaryKey(),
|
|
14
|
-
slug: text("slug").notNull().unique("link_pages_slug_key"),
|
|
15
|
-
title: text("title").notNull(),
|
|
16
|
-
description: text("description"),
|
|
17
|
-
coverImageUrl: text("coverImageUrl"),
|
|
18
|
-
releaseId: integer("releaseId").references(() => releases.id, { onDelete: "set null" }),
|
|
19
|
-
isPublished: boolean("isPublished").notNull().default(true),
|
|
20
|
-
createdAt: timestamp("createdAt", { mode: "date", precision: 3 }).notNull().defaultNow(),
|
|
21
|
-
updatedAt: timestamp("updatedAt", { mode: "date", precision: 3 })
|
|
22
|
-
.notNull()
|
|
23
|
-
.$defaultFn(() => new Date())
|
|
24
|
-
.$onUpdateFn(() => new Date()),
|
|
25
|
-
...extras,
|
|
26
|
-
},
|
|
27
|
-
(t) => [index("link_pages_releaseId_idx").on(t.releaseId)],
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const linkPages = buildLinkPages();
|
|
32
|
-
|
|
33
|
-
export function buildLinkPageItems<TExtras extends ExtraColumns = {}>(
|
|
34
|
-
extras: TExtras = {} as TExtras,
|
|
35
|
-
) {
|
|
36
|
-
return pgTable(
|
|
37
|
-
"link_page_items",
|
|
38
|
-
{
|
|
39
|
-
id: serial("id").primaryKey(),
|
|
40
|
-
pageId: integer("pageId").notNull().references(() => linkPages.id, { onDelete: "cascade" }),
|
|
41
|
-
title: text("title").notNull(),
|
|
42
|
-
url: text("url").notNull(),
|
|
43
|
-
position: integer("position").notNull().default(0),
|
|
44
|
-
isVisible: boolean("isVisible").notNull().default(true),
|
|
45
|
-
createdAt: timestamp("createdAt", { mode: "date", precision: 3 }).notNull().defaultNow(),
|
|
46
|
-
updatedAt: timestamp("updatedAt", { mode: "date", precision: 3 })
|
|
47
|
-
.notNull()
|
|
48
|
-
.$defaultFn(() => new Date())
|
|
49
|
-
.$onUpdateFn(() => new Date()),
|
|
50
|
-
...extras,
|
|
51
|
-
},
|
|
52
|
-
(t) => [index("link_page_items_pageId_idx").on(t.pageId)],
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export const linkPageItems = buildLinkPageItems();
|
package/src/schema/relations.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { relations } from "drizzle-orm";
|
|
2
|
-
import type { PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import { releases as baseReleases } from "@gigamusic/db/schema";
|
|
4
|
-
import {
|
|
5
|
-
linkPages as baseLinkPages,
|
|
6
|
-
linkPageItems as baseLinkPageItems,
|
|
7
|
-
} from "./link-pages";
|
|
8
|
-
|
|
9
|
-
// `PgTable<any>` so consumer concrete tables satisfy these factory params —
|
|
10
|
-
// see @gigamusic/db's types.ts for the variance rationale.
|
|
11
|
-
type AnyPgTableValue = PgTable<any>;
|
|
12
|
-
|
|
13
|
-
export function createLinkPagesRelations(t: {
|
|
14
|
-
linkPages: AnyPgTableValue;
|
|
15
|
-
linkPageItems: AnyPgTableValue;
|
|
16
|
-
releases: AnyPgTableValue;
|
|
17
|
-
}) {
|
|
18
|
-
const linkPages = t.linkPages as unknown as typeof baseLinkPages;
|
|
19
|
-
const linkPageItems = t.linkPageItems as unknown as typeof baseLinkPageItems;
|
|
20
|
-
const releases = t.releases as unknown as typeof baseReleases;
|
|
21
|
-
return relations(linkPages, ({ one, many }) => ({
|
|
22
|
-
release: one(releases, {
|
|
23
|
-
fields: [linkPages.releaseId],
|
|
24
|
-
references: [releases.id],
|
|
25
|
-
}),
|
|
26
|
-
items: many(linkPageItems),
|
|
27
|
-
}));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function createLinkPageItemsRelations(t: {
|
|
31
|
-
linkPages: AnyPgTableValue;
|
|
32
|
-
linkPageItems: AnyPgTableValue;
|
|
33
|
-
}) {
|
|
34
|
-
const linkPages = t.linkPages as unknown as typeof baseLinkPages;
|
|
35
|
-
const linkPageItems = t.linkPageItems as unknown as typeof baseLinkPageItems;
|
|
36
|
-
return relations(linkPageItems, ({ one }) => ({
|
|
37
|
-
page: one(linkPages, {
|
|
38
|
-
fields: [linkPageItems.pageId],
|
|
39
|
-
references: [linkPages.id],
|
|
40
|
-
}),
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Convenience top-level exports bound to the un-extended convenience tables.
|
|
45
|
-
export const linkPagesRelations = createLinkPagesRelations({
|
|
46
|
-
linkPages: baseLinkPages,
|
|
47
|
-
linkPageItems: baseLinkPageItems,
|
|
48
|
-
releases: baseReleases,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
export const linkPageItemsRelations = createLinkPageItemsRelations({
|
|
52
|
-
linkPages: baseLinkPages,
|
|
53
|
-
linkPageItems: baseLinkPageItems,
|
|
54
|
-
});
|
package/src/types.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { PgTable } from "drizzle-orm/pg-core";
|
|
2
|
-
import type { LinkPageInput, LinkPageItemInput } from "@gigamusic/core";
|
|
3
|
-
import type * as linksSchema from "./schema/index";
|
|
4
|
-
|
|
5
|
-
export type { LinkPageInput, LinkPageItemInput };
|
|
6
|
-
|
|
7
|
-
// `PgTable<any>` (not `PgTable<TableConfig>`) so consumer concrete tables
|
|
8
|
-
// satisfy this constraint — Drizzle's column generics are invariant on
|
|
9
|
-
// column shape; see @gigamusic/db's types.ts for the full rationale.
|
|
10
|
-
type AnyPgTableValue = PgTable<any>;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The bag of Drizzle table identities the link-page queries factory closes
|
|
14
|
-
* over. Consumers can pass extended versions of these tables built via the
|
|
15
|
-
* `buildLinkPages` / `buildLinkPageItems` factories; the `releases` reference
|
|
16
|
-
* is the consumer's `releases` table (for the cover-image fallback join).
|
|
17
|
-
*/
|
|
18
|
-
export interface LinkPageQueryTables {
|
|
19
|
-
linkPages: AnyPgTableValue;
|
|
20
|
-
linkPageItems: AnyPgTableValue;
|
|
21
|
-
releases: AnyPgTableValue;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface DefaultLinkPageTables extends LinkPageQueryTables {
|
|
25
|
-
linkPages: typeof linksSchema.linkPages;
|
|
26
|
-
linkPageItems: typeof linksSchema.linkPageItems;
|
|
27
|
-
// `releases` defaults to the gigamusic convenience table; consumers pass
|
|
28
|
-
// their own extended releases when they have one.
|
|
29
|
-
releases: AnyPgTableValue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** A single link button rendered inside a `LinkPage`. */
|
|
33
|
-
export type LinkPageItem<T extends LinkPageQueryTables = DefaultLinkPageTables> =
|
|
34
|
-
T["linkPageItems"]["$inferSelect"];
|
|
35
|
-
|
|
36
|
-
/** Optional `Release` reference exposed to the public view for cover fallback. */
|
|
37
|
-
export interface LinkPageReleaseRef {
|
|
38
|
-
id: number;
|
|
39
|
-
name: string;
|
|
40
|
-
slug: string;
|
|
41
|
-
coverImageUrl: string | null;
|
|
42
|
-
isPublished: boolean;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Top-level "Linktree-style" page row. */
|
|
46
|
-
export type LinkPage<T extends LinkPageQueryTables = DefaultLinkPageTables> =
|
|
47
|
-
T["linkPages"]["$inferSelect"];
|
|
48
|
-
|
|
49
|
-
/** A `LinkPage` joined with its visible (or all) items and an optional release ref. */
|
|
50
|
-
export type LinkPageWithItems<T extends LinkPageQueryTables = DefaultLinkPageTables> =
|
|
51
|
-
LinkPage<T> & {
|
|
52
|
-
items: LinkPageItem<T>[];
|
|
53
|
-
release?: LinkPageReleaseRef | null;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Query helpers that close over a Drizzle DB. Consumers merge this with
|
|
58
|
-
* `@gigamusic/db`'s `Queries` factory result (or pass it through wherever a
|
|
59
|
-
* `LinkPageQueries` is needed).
|
|
60
|
-
*/
|
|
61
|
-
export interface LinkPageQueries<T extends LinkPageQueryTables = DefaultLinkPageTables> {
|
|
62
|
-
listPublishedLinkPages(): Promise<LinkPage<T>[]>;
|
|
63
|
-
listAllLinkPages(): Promise<LinkPage<T>[]>;
|
|
64
|
-
getPublicLinkPageBySlug(slug: string): Promise<LinkPageWithItems<T> | null>;
|
|
65
|
-
getLinkPageById(id: number): Promise<LinkPageWithItems<T> | null>;
|
|
66
|
-
createLinkPage(input: LinkPageInput): Promise<LinkPage<T>>;
|
|
67
|
-
updateLinkPage(id: number, input: Partial<LinkPageInput>): Promise<LinkPage<T>>;
|
|
68
|
-
deleteLinkPage(id: number): Promise<void>;
|
|
69
|
-
addLinkPageItem(pageId: number, input: LinkPageItemInput): Promise<LinkPageItem<T>>;
|
|
70
|
-
updateLinkPageItem(
|
|
71
|
-
id: number,
|
|
72
|
-
input: Partial<LinkPageItemInput>,
|
|
73
|
-
): Promise<LinkPageItem<T>>;
|
|
74
|
-
deleteLinkPageItem(id: number): Promise<void>;
|
|
75
|
-
reorderLinkPageItems(pageId: number, orderedItemIds: number[]): Promise<void>;
|
|
76
|
-
}
|