@saena-io/content 0.1.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.
@@ -0,0 +1,16 @@
1
+ -- Retire the old block-CMS table + its migration history (renamed plugin cms → content). The dev DB had no
2
+ -- production data; this leaves no confusing remnants of the superseded block model (ADR-0007 → ADR-0008).
3
+ DROP TABLE IF EXISTS "app"."cms_pages";
4
+ --> statement-breakpoint
5
+ DROP TABLE IF EXISTS "drizzle"."__migrations_cms";
6
+ --> statement-breakpoint
7
+ CREATE TABLE "app"."content_section_values" (
8
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
9
+ "page" text NOT NULL,
10
+ "section" text NOT NULL,
11
+ "config" jsonb DEFAULT '{}'::jsonb NOT NULL,
12
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
13
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
14
+ );
15
+ --> statement-breakpoint
16
+ CREATE UNIQUE INDEX "content_section_values_page_section_uq" ON "app"."content_section_values" USING btree ("page","section");
@@ -0,0 +1,12 @@
1
+ CREATE TABLE "app"."content_collection_items" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "collection" text NOT NULL,
4
+ "slug" text NOT NULL,
5
+ "config" jsonb DEFAULT '{}'::jsonb NOT NULL,
6
+ "sort_order" integer DEFAULT 0 NOT NULL,
7
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
8
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE UNIQUE INDEX "content_collection_items_collection_slug_uq" ON "app"."content_collection_items" USING btree ("collection","slug");--> statement-breakpoint
12
+ CREATE INDEX "content_collection_items_collection_order_idx" ON "app"."content_collection_items" USING btree ("collection","sort_order");
@@ -0,0 +1,94 @@
1
+ {
2
+ "id": "ac4e978b-3097-4c2a-8ec7-51a07e58741a",
3
+ "prevId": "00000000-0000-0000-0000-000000000000",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "app.content_section_values": {
8
+ "name": "content_section_values",
9
+ "schema": "app",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "gen_random_uuid()"
17
+ },
18
+ "page": {
19
+ "name": "page",
20
+ "type": "text",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "section": {
25
+ "name": "section",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "config": {
31
+ "name": "config",
32
+ "type": "jsonb",
33
+ "primaryKey": false,
34
+ "notNull": true,
35
+ "default": "'{}'::jsonb"
36
+ },
37
+ "created_at": {
38
+ "name": "created_at",
39
+ "type": "timestamp with time zone",
40
+ "primaryKey": false,
41
+ "notNull": true,
42
+ "default": "now()"
43
+ },
44
+ "updated_at": {
45
+ "name": "updated_at",
46
+ "type": "timestamp with time zone",
47
+ "primaryKey": false,
48
+ "notNull": true,
49
+ "default": "now()"
50
+ }
51
+ },
52
+ "indexes": {
53
+ "content_section_values_page_section_uq": {
54
+ "name": "content_section_values_page_section_uq",
55
+ "columns": [
56
+ {
57
+ "expression": "page",
58
+ "isExpression": false,
59
+ "asc": true,
60
+ "nulls": "last"
61
+ },
62
+ {
63
+ "expression": "section",
64
+ "isExpression": false,
65
+ "asc": true,
66
+ "nulls": "last"
67
+ }
68
+ ],
69
+ "isUnique": true,
70
+ "concurrently": false,
71
+ "method": "btree",
72
+ "with": {}
73
+ }
74
+ },
75
+ "foreignKeys": {},
76
+ "compositePrimaryKeys": {},
77
+ "uniqueConstraints": {},
78
+ "policies": {},
79
+ "checkConstraints": {},
80
+ "isRLSEnabled": false
81
+ }
82
+ },
83
+ "enums": {},
84
+ "schemas": {},
85
+ "sequences": {},
86
+ "roles": {},
87
+ "policies": {},
88
+ "views": {},
89
+ "_meta": {
90
+ "columns": {},
91
+ "schemas": {},
92
+ "tables": {}
93
+ }
94
+ }
@@ -0,0 +1,197 @@
1
+ {
2
+ "id": "2e43870d-06a3-460b-87ce-acefeafda986",
3
+ "prevId": "ac4e978b-3097-4c2a-8ec7-51a07e58741a",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "app.content_collection_items": {
8
+ "name": "content_collection_items",
9
+ "schema": "app",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "gen_random_uuid()"
17
+ },
18
+ "collection": {
19
+ "name": "collection",
20
+ "type": "text",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "slug": {
25
+ "name": "slug",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "config": {
31
+ "name": "config",
32
+ "type": "jsonb",
33
+ "primaryKey": false,
34
+ "notNull": true,
35
+ "default": "'{}'::jsonb"
36
+ },
37
+ "sort_order": {
38
+ "name": "sort_order",
39
+ "type": "integer",
40
+ "primaryKey": false,
41
+ "notNull": true,
42
+ "default": 0
43
+ },
44
+ "created_at": {
45
+ "name": "created_at",
46
+ "type": "timestamp with time zone",
47
+ "primaryKey": false,
48
+ "notNull": true,
49
+ "default": "now()"
50
+ },
51
+ "updated_at": {
52
+ "name": "updated_at",
53
+ "type": "timestamp with time zone",
54
+ "primaryKey": false,
55
+ "notNull": true,
56
+ "default": "now()"
57
+ }
58
+ },
59
+ "indexes": {
60
+ "content_collection_items_collection_slug_uq": {
61
+ "name": "content_collection_items_collection_slug_uq",
62
+ "columns": [
63
+ {
64
+ "expression": "collection",
65
+ "isExpression": false,
66
+ "asc": true,
67
+ "nulls": "last"
68
+ },
69
+ {
70
+ "expression": "slug",
71
+ "isExpression": false,
72
+ "asc": true,
73
+ "nulls": "last"
74
+ }
75
+ ],
76
+ "isUnique": true,
77
+ "concurrently": false,
78
+ "method": "btree",
79
+ "with": {}
80
+ },
81
+ "content_collection_items_collection_order_idx": {
82
+ "name": "content_collection_items_collection_order_idx",
83
+ "columns": [
84
+ {
85
+ "expression": "collection",
86
+ "isExpression": false,
87
+ "asc": true,
88
+ "nulls": "last"
89
+ },
90
+ {
91
+ "expression": "sort_order",
92
+ "isExpression": false,
93
+ "asc": true,
94
+ "nulls": "last"
95
+ }
96
+ ],
97
+ "isUnique": false,
98
+ "concurrently": false,
99
+ "method": "btree",
100
+ "with": {}
101
+ }
102
+ },
103
+ "foreignKeys": {},
104
+ "compositePrimaryKeys": {},
105
+ "uniqueConstraints": {},
106
+ "policies": {},
107
+ "checkConstraints": {},
108
+ "isRLSEnabled": false
109
+ },
110
+ "app.content_section_values": {
111
+ "name": "content_section_values",
112
+ "schema": "app",
113
+ "columns": {
114
+ "id": {
115
+ "name": "id",
116
+ "type": "uuid",
117
+ "primaryKey": true,
118
+ "notNull": true,
119
+ "default": "gen_random_uuid()"
120
+ },
121
+ "page": {
122
+ "name": "page",
123
+ "type": "text",
124
+ "primaryKey": false,
125
+ "notNull": true
126
+ },
127
+ "section": {
128
+ "name": "section",
129
+ "type": "text",
130
+ "primaryKey": false,
131
+ "notNull": true
132
+ },
133
+ "config": {
134
+ "name": "config",
135
+ "type": "jsonb",
136
+ "primaryKey": false,
137
+ "notNull": true,
138
+ "default": "'{}'::jsonb"
139
+ },
140
+ "created_at": {
141
+ "name": "created_at",
142
+ "type": "timestamp with time zone",
143
+ "primaryKey": false,
144
+ "notNull": true,
145
+ "default": "now()"
146
+ },
147
+ "updated_at": {
148
+ "name": "updated_at",
149
+ "type": "timestamp with time zone",
150
+ "primaryKey": false,
151
+ "notNull": true,
152
+ "default": "now()"
153
+ }
154
+ },
155
+ "indexes": {
156
+ "content_section_values_page_section_uq": {
157
+ "name": "content_section_values_page_section_uq",
158
+ "columns": [
159
+ {
160
+ "expression": "page",
161
+ "isExpression": false,
162
+ "asc": true,
163
+ "nulls": "last"
164
+ },
165
+ {
166
+ "expression": "section",
167
+ "isExpression": false,
168
+ "asc": true,
169
+ "nulls": "last"
170
+ }
171
+ ],
172
+ "isUnique": true,
173
+ "concurrently": false,
174
+ "method": "btree",
175
+ "with": {}
176
+ }
177
+ },
178
+ "foreignKeys": {},
179
+ "compositePrimaryKeys": {},
180
+ "uniqueConstraints": {},
181
+ "policies": {},
182
+ "checkConstraints": {},
183
+ "isRLSEnabled": false
184
+ }
185
+ },
186
+ "enums": {},
187
+ "schemas": {},
188
+ "sequences": {},
189
+ "roles": {},
190
+ "policies": {},
191
+ "views": {},
192
+ "_meta": {
193
+ "columns": {},
194
+ "schemas": {},
195
+ "tables": {}
196
+ }
197
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1782341279704,
9
+ "tag": "0000_tranquil_golden_guardian",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1782477956077,
16
+ "tag": "0001_little_darwin",
17
+ "breakpoints": true
18
+ }
19
+ ]
20
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@saena-io/content",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./contract": "./src/contract.ts",
10
+ "./field": "./src/field/index.ts",
11
+ "./define": "./src/define.ts",
12
+ "./admin": "./src/admin/index.tsx",
13
+ "./public": "./src/public/index.tsx"
14
+ },
15
+ "scripts": {
16
+ "build": "echo \"@saena-io/content is consumed as source in the monorepo; publish build is deferred (ADR-0006)\"",
17
+ "db:generate": "drizzle-kit generate",
18
+ "test": "vitest run --passWithNoTests",
19
+ "lint": "biome check .",
20
+ "typecheck": "tsc --noEmit",
21
+ "clean": "rm -rf dist .turbo"
22
+ },
23
+ "dependencies": {
24
+ "@saena-io/plugin-sdk": "workspace:*",
25
+ "@saena-io/ui": "workspace:*",
26
+ "drizzle-orm": "^0.45.2",
27
+ "react": "^19.2.6",
28
+ "zod": "^4.4.3"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^19",
32
+ "drizzle-kit": "^0.31.10"
33
+ },
34
+ "files": [
35
+ "src",
36
+ "drizzle"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }
@@ -0,0 +1,43 @@
1
+ import { type CollectionSaveInputDto, type SectionSaveInputDto, contentFns } from '../contract';
2
+
3
+ /** One collection entry as the admin client receives it (value shape is per-collection — the caller types it). */
4
+ export interface CollectionItemDto {
5
+ id: string;
6
+ slug: string;
7
+ value: unknown;
8
+ }
9
+
10
+ // Typed client for the content API (ADR-0006): POST /api/plugin/<fnId>. The section structure is read from the
11
+ // registry directly (./define); this client is only the value read/write. Generalises into @saena-io/admin later.
12
+ async function call<T>(fnId: string, input: unknown): Promise<T> {
13
+ const res = await fetch(`/api/plugin/${fnId}`, {
14
+ method: 'POST',
15
+ headers: { 'content-type': 'application/json' },
16
+ body: JSON.stringify(input ?? {}),
17
+ });
18
+ if (!res.ok) {
19
+ const body = (await res.json().catch(() => null)) as { error?: string } | null;
20
+ throw new Error(body?.error ?? `plugin call failed (${res.status})`);
21
+ }
22
+ return (await res.json()) as T;
23
+ }
24
+
25
+ export const contentClient = {
26
+ /** A section's hydrated value (main-locale source for editing). Shape is per-section (the caller types it). */
27
+ getSection: (page: string, section: string) =>
28
+ call<unknown>(contentFns.sectionGet, { page, section }),
29
+ saveSection: (input: SectionSaveInputDto) => call<{ ok: true }>(contentFns.sectionSave, input),
30
+
31
+ /** Routed collections (ADR-0011) — the manager's CRUD over a collection's entries. */
32
+ collection: {
33
+ list: (collection: string) =>
34
+ call<CollectionItemDto[]>(contentFns.collectionList, { collection }),
35
+ getForEdit: (collection: string, id: string) =>
36
+ call<CollectionItemDto | null>(contentFns.collectionGetForEdit, { collection, id }),
37
+ save: (input: CollectionSaveInputDto) => call<{ id: string }>(contentFns.collectionSave, input),
38
+ delete: (collection: string, id: string) =>
39
+ call<{ ok: true }>(contentFns.collectionDelete, { collection, id }),
40
+ reorder: (collection: string, orderedIds: string[]) =>
41
+ call<{ ok: true }>(contentFns.collectionReorder, { collection, orderedIds }),
42
+ },
43
+ };