opacacms 0.1.1 โ†’ 0.1.2

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.
Files changed (212) hide show
  1. package/package.json +36 -1
  2. package/bun.lock +0 -34
  3. package/global.d.ts +0 -11
  4. package/src/admin/api-client.ts +0 -63
  5. package/src/admin/auth-client.ts +0 -40
  6. package/src/admin/custom-field.ts +0 -179
  7. package/src/admin/index.ts +0 -15
  8. package/src/admin/react.tsx +0 -72
  9. package/src/admin/router.ts +0 -9
  10. package/src/admin/stores/admin-queries.ts +0 -121
  11. package/src/admin/stores/auth.ts +0 -61
  12. package/src/admin/stores/column-visibility.ts +0 -67
  13. package/src/admin/stores/config.ts +0 -15
  14. package/src/admin/stores/media.ts +0 -95
  15. package/src/admin/stores/query.ts +0 -13
  16. package/src/admin/stores/ui.ts +0 -29
  17. package/src/admin/ui/admin-client.tsx +0 -283
  18. package/src/admin/ui/admin-layout.tsx +0 -276
  19. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  20. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  21. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  22. package/src/admin/ui/components/Table.tsx +0 -67
  23. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  24. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  25. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  26. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  27. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  28. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  29. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  30. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  31. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  32. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  33. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  34. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  35. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  36. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  37. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  38. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  39. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  40. package/src/admin/ui/components/fields/index.tsx +0 -371
  41. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  42. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  43. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  44. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  45. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  46. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  47. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  48. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  49. package/src/admin/ui/components/fields/utils.ts +0 -1
  50. package/src/admin/ui/components/link.tsx +0 -41
  51. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  52. package/src/admin/ui/components/toast.tsx +0 -72
  53. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  54. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  55. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  56. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  57. package/src/admin/ui/components/ui/button.tsx +0 -26
  58. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  59. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  60. package/src/admin/ui/components/ui/group.tsx +0 -20
  61. package/src/admin/ui/components/ui/index.ts +0 -17
  62. package/src/admin/ui/components/ui/input.tsx +0 -12
  63. package/src/admin/ui/components/ui/join.tsx +0 -53
  64. package/src/admin/ui/components/ui/label.tsx +0 -11
  65. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  66. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  67. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  68. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  69. package/src/admin/ui/components/ui/select.tsx +0 -187
  70. package/src/admin/ui/components/ui/separator.tsx +0 -21
  71. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  72. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  73. package/src/admin/ui/components/ui/utils.ts +0 -3
  74. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  75. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  76. package/src/admin/ui/styles/accordion.scss +0 -60
  77. package/src/admin/ui/styles/animations.scss +0 -41
  78. package/src/admin/ui/styles/asset-manager.scss +0 -547
  79. package/src/admin/ui/styles/badge.scss +0 -13
  80. package/src/admin/ui/styles/base.scss +0 -22
  81. package/src/admin/ui/styles/button.scss +0 -161
  82. package/src/admin/ui/styles/card.scss +0 -13
  83. package/src/admin/ui/styles/collapsible.scss +0 -75
  84. package/src/admin/ui/styles/data-detail.scss +0 -92
  85. package/src/admin/ui/styles/dialog.scss +0 -102
  86. package/src/admin/ui/styles/empty-state.scss +0 -22
  87. package/src/admin/ui/styles/group.scss +0 -19
  88. package/src/admin/ui/styles/index.scss +0 -33
  89. package/src/admin/ui/styles/input.scss +0 -80
  90. package/src/admin/ui/styles/label.scss +0 -12
  91. package/src/admin/ui/styles/layout.scss +0 -56
  92. package/src/admin/ui/styles/lexical.scss +0 -469
  93. package/src/admin/ui/styles/loading.scss +0 -102
  94. package/src/admin/ui/styles/media-registry.scss +0 -597
  95. package/src/admin/ui/styles/pagination.scss +0 -20
  96. package/src/admin/ui/styles/radio-group.scss +0 -66
  97. package/src/admin/ui/styles/row.scss +0 -17
  98. package/src/admin/ui/styles/scrollbar.scss +0 -36
  99. package/src/admin/ui/styles/select.scss +0 -121
  100. package/src/admin/ui/styles/separator.scss +0 -14
  101. package/src/admin/ui/styles/sheet.scss +0 -152
  102. package/src/admin/ui/styles/sidebar.scss +0 -148
  103. package/src/admin/ui/styles/switch.scss +0 -59
  104. package/src/admin/ui/styles/table.scss +0 -207
  105. package/src/admin/ui/styles/tabs.scss +0 -62
  106. package/src/admin/ui/styles/toast.scss +0 -45
  107. package/src/admin/ui/styles/variables.scss +0 -24
  108. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  109. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  110. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  111. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  112. package/src/admin/ui/views/init-view.tsx +0 -182
  113. package/src/admin/ui/views/login-view.tsx +0 -123
  114. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  115. package/src/admin/ui/views/settings-view.tsx +0 -729
  116. package/src/admin/webcomponent.tsx +0 -15
  117. package/src/auth/index.ts +0 -194
  118. package/src/auth/migrations.ts +0 -87
  119. package/src/auth/premissions.ts +0 -46
  120. package/src/cli/commands/generate-types.ts +0 -116
  121. package/src/cli/commands/init.ts +0 -95
  122. package/src/cli/commands/migrate-commands.ts +0 -160
  123. package/src/cli/commands/seed-command.ts +0 -11
  124. package/src/cli/d1-mock.ts +0 -101
  125. package/src/cli/index.test.ts +0 -84
  126. package/src/cli/index.ts +0 -183
  127. package/src/cli/r2-mock.ts +0 -217
  128. package/src/cli/seeding.ts +0 -409
  129. package/src/client.ts +0 -181
  130. package/src/config-utils.ts +0 -102
  131. package/src/config.ts +0 -49
  132. package/src/db/adapter.ts +0 -53
  133. package/src/db/better-sqlite.ts +0 -630
  134. package/src/db/bun-sqlite.ts +0 -646
  135. package/src/db/d1.ts +0 -711
  136. package/src/db/index.ts +0 -2
  137. package/src/db/kysely/data-mapper.ts +0 -142
  138. package/src/db/kysely/field-mapper.ts +0 -148
  139. package/src/db/kysely/migration-generator.ts +0 -223
  140. package/src/db/kysely/query-builder.ts +0 -92
  141. package/src/db/kysely/schema-builder.ts +0 -439
  142. package/src/db/kysely/sql-utils.ts +0 -13
  143. package/src/db/migration.ts +0 -40
  144. package/src/db/postgres.ts +0 -621
  145. package/src/db/sqlite.ts +0 -658
  146. package/src/db/system-schema.ts +0 -121
  147. package/src/index.ts +0 -11
  148. package/src/runtimes/README.md +0 -59
  149. package/src/runtimes/bun.ts +0 -49
  150. package/src/runtimes/cloudflare-workers.ts +0 -38
  151. package/src/runtimes/next.ts +0 -26
  152. package/src/runtimes/node.ts +0 -52
  153. package/src/schema/collection.ts +0 -184
  154. package/src/schema/fields/base.ts +0 -164
  155. package/src/schema/fields/index.ts +0 -427
  156. package/src/schema/global.ts +0 -145
  157. package/src/schema/index.ts +0 -4
  158. package/src/schema/infer.ts +0 -72
  159. package/src/server/admin-router.ts +0 -20
  160. package/src/server/admin.ts +0 -142
  161. package/src/server/assets.ts +0 -306
  162. package/src/server/collection-router.ts +0 -55
  163. package/src/server/handlers.ts +0 -722
  164. package/src/server/middlewares/admin.ts +0 -27
  165. package/src/server/middlewares/auth.ts +0 -89
  166. package/src/server/middlewares/context.ts +0 -17
  167. package/src/server/middlewares/cors.ts +0 -24
  168. package/src/server/middlewares/database-init.ts +0 -74
  169. package/src/server/middlewares/rate-limit.ts +0 -71
  170. package/src/server/router.ts +0 -47
  171. package/src/server/setup-middlewares.ts +0 -58
  172. package/src/server/system-router.ts +0 -35
  173. package/src/server.ts +0 -9
  174. package/src/storage/adapters/cloudflare-r2.ts +0 -136
  175. package/src/storage/adapters/local.ts +0 -146
  176. package/src/storage/adapters/s3.ts +0 -186
  177. package/src/storage/errors.ts +0 -46
  178. package/src/storage/index.ts +0 -6
  179. package/src/storage/types.ts +0 -39
  180. package/src/types.ts +0 -605
  181. package/src/utils/lexical.ts +0 -37
  182. package/src/utils/logger.ts +0 -73
  183. package/src/validation.ts +0 -429
  184. package/src/validator.ts +0 -179
  185. package/test/admin-custom-field.test.ts +0 -162
  186. package/test/admin-react-field.test.tsx +0 -134
  187. package/test/api-features.test.ts +0 -78
  188. package/test/api.test.ts +0 -178
  189. package/test/auth.test.ts +0 -62
  190. package/test/cli-integration.test.ts +0 -148
  191. package/test/cli.test.ts +0 -25
  192. package/test/db/postgres.test.ts +0 -95
  193. package/test/db/sqlite-filter.test.ts +0 -53
  194. package/test/db/sqlite.test.ts +0 -82
  195. package/test/engine-features.test.ts +0 -79
  196. package/test/globals.test.ts +0 -74
  197. package/test/integration-tmp/db-app/opacacms.config.ts +0 -15
  198. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +0 -25
  199. package/test/integration-tmp/my-test-app/index.ts +0 -8
  200. package/test/integration-tmp/my-test-app/opacacms.config.ts +0 -16
  201. package/test/integration-tmp/my-test-app/package.json +0 -12
  202. package/test/populate.test.ts +0 -79
  203. package/test/runtimes.test.ts +0 -43
  204. package/test/schema-builder.test.ts +0 -107
  205. package/test/schema-features.test.ts +0 -63
  206. package/test/seeding.test.ts +0 -68
  207. package/test/storage/local.test.ts +0 -72
  208. package/test/storage/s3.test.ts +0 -60
  209. package/test/structural-data.test.ts +0 -100
  210. package/test/test-setup.ts +0 -11
  211. package/test/validation.test.ts +0 -162
  212. package/tsconfig.json +0 -42
@@ -1,217 +0,0 @@
1
- import { Database } from "bun:sqlite";
2
- import crypto from "node:crypto";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
-
6
- /**
7
- * A minimal R2Bucket mock for OpacaCMS CLI that uses Wrangler's local state.
8
- * Stores metadata in _mf_objects table and blobs in .blobs sibling directory.
9
- */
10
- export function createR2Mock(dbPath?: string) {
11
- let sqlite: Database;
12
- let blobsDir: string;
13
- let finalDbPath: string | undefined;
14
-
15
- // 1. Try to find Wrangler's local R2 state if no path provided
16
- const wranglerR2Dir = path.resolve(
17
- process.cwd(),
18
- ".wrangler/state/v3/r2/miniflare-R2BucketObject",
19
- );
20
-
21
- if (!dbPath && fs.existsSync(wranglerR2Dir)) {
22
- const files = fs.readdirSync(wranglerR2Dir);
23
- const sqliteFile = files.find((f) => f.endsWith(".sqlite"));
24
- if (sqliteFile) {
25
- finalDbPath = path.join(wranglerR2Dir, sqliteFile);
26
- console.log(`[OpacaCMS] Using Wrangler R2 local state: ${sqliteFile}`);
27
- }
28
- }
29
-
30
- // 2. Handle specific path or fallback
31
- if (!finalDbPath) {
32
- const inputPath = dbPath || ".opaca/local-r2/mock-bucket.sqlite";
33
- const absolutePath = path.isAbsolute(inputPath)
34
- ? inputPath
35
- : path.resolve(process.cwd(), inputPath);
36
-
37
- // If it's a directory, append default filename
38
- if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
39
- finalDbPath = path.join(absolutePath, "mock-bucket.sqlite");
40
- } else if (!absolutePath.endsWith(".sqlite")) {
41
- finalDbPath = absolutePath.endsWith("/")
42
- ? path.join(absolutePath, "mock-bucket.sqlite")
43
- : absolutePath + ".sqlite";
44
- } else {
45
- finalDbPath = absolutePath;
46
- }
47
- console.log(`[OpacaCMS] Using local R2 mock: ${path.basename(finalDbPath)}`);
48
- }
49
-
50
- // Ensure directory exists
51
- const dir = path.dirname(finalDbPath);
52
- if (!fs.existsSync(dir)) {
53
- fs.mkdirSync(dir, { recursive: true });
54
- }
55
-
56
- try {
57
- sqlite = new Database(finalDbPath);
58
- } catch (err: any) {
59
- throw new Error(`Failed to open R2 mock database at ${finalDbPath}: ${err.message}`);
60
- }
61
-
62
- blobsDir = finalDbPath.replace(".sqlite", ".blobs");
63
- if (!fs.existsSync(blobsDir)) {
64
- fs.mkdirSync(dir, { recursive: true });
65
- fs.mkdirSync(blobsDir, { recursive: true });
66
- }
67
-
68
- // Ensure table exists (miniflare 3 schema)
69
- sqlite.exec(`
70
- CREATE TABLE IF NOT EXISTS _mf_objects (
71
- key TEXT PRIMARY KEY,
72
- blob_id TEXT,
73
- version TEXT,
74
- size INTEGER,
75
- etag TEXT,
76
- uploaded INTEGER,
77
- checksums TEXT,
78
- http_metadata TEXT,
79
- custom_metadata TEXT
80
- )
81
- `);
82
-
83
- const getBlobPath = (blobId: string) => path.join(blobsDir, blobId);
84
-
85
- return {
86
- async put(key: string, value: any, options?: any) {
87
- const blobId = crypto.randomUUID();
88
- const filePath = getBlobPath(blobId);
89
-
90
- const buffer =
91
- value instanceof Uint8Array
92
- ? value
93
- : value instanceof ArrayBuffer
94
- ? new Uint8Array(value)
95
- : typeof value === "string" || value instanceof Buffer
96
- ? Buffer.from(value)
97
- : new Uint8Array(await (value as any).arrayBuffer());
98
-
99
- fs.writeFileSync(filePath, buffer);
100
-
101
- // MD5 for ETag (standard for R2/Miniflare)
102
- const etag = crypto.createHash("md5").update(buffer).digest("hex");
103
-
104
- // Miniflare 3 internal SQLite state often use MILLIS for 'uploaded'
105
- // despite the R2 API returning seconds. Let's use MILLIS.
106
- const uploaded = Date.now();
107
- const size = buffer.length;
108
-
109
- // Ensure we use the exact keys expected by Workerd/Miniflare
110
- const httpMetadataObj: Record<string, string> = {};
111
- if (options?.httpMetadata?.contentType) {
112
- httpMetadataObj.contentType = options.httpMetadata.contentType;
113
- }
114
-
115
- const customMetadataObj: Record<string, string> = { ...options?.customMetadata };
116
- if (options?.customMetadata?.sourceUrl) {
117
- customMetadataObj.sourceUrl = options.customMetadata.sourceUrl;
118
- }
119
-
120
- const httpMetadata = JSON.stringify(httpMetadataObj);
121
- const customMetadata = JSON.stringify(customMetadataObj);
122
-
123
- sqlite
124
- .prepare(`
125
- INSERT OR REPLACE INTO _mf_objects
126
- (key, blob_id, version, size, etag, uploaded, checksums, http_metadata, custom_metadata)
127
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
128
- `)
129
- .run(key, blobId, "v1", size, etag, uploaded, "{}", httpMetadata, customMetadata);
130
-
131
- return {
132
- key,
133
- size,
134
- etag,
135
- httpMetadata: options?.httpMetadata || {},
136
- customMetadata: options?.customMetadata || {},
137
- };
138
- },
139
-
140
- async get(key: string) {
141
- const row = sqlite.prepare("SELECT * FROM _mf_objects WHERE key = ?").get(key) as any;
142
- if (!row) return null;
143
-
144
- const filePath = getBlobPath(row.blob_id);
145
- if (!fs.existsSync(filePath)) return null;
146
-
147
- const buffer = fs.readFileSync(filePath);
148
- const stream = new ReadableStream({
149
- start(controller) {
150
- controller.enqueue(new Uint8Array(buffer));
151
- controller.close();
152
- },
153
- });
154
-
155
- return {
156
- key: row.key,
157
- size: row.size,
158
- etag: row.etag,
159
- uploaded: new Date(row.uploaded), // Works with both s and ms
160
- httpMetadata: JSON.parse(row.http_metadata || "{}"),
161
- customMetadata: JSON.parse(row.custom_metadata || "{}"),
162
- body: stream,
163
- bodyUsed: false,
164
- async arrayBuffer() {
165
- return buffer.buffer;
166
- },
167
- async text() {
168
- return buffer.toString();
169
- },
170
- async json() {
171
- return JSON.parse(buffer.toString());
172
- },
173
- };
174
- },
175
-
176
- async head(key: string) {
177
- const row = sqlite
178
- .prepare(
179
- "SELECT key, size, etag, http_metadata, custom_metadata, uploaded FROM _mf_objects WHERE key = ?",
180
- )
181
- .get(key) as any;
182
- if (!row) return null;
183
- return {
184
- key: row.key,
185
- size: row.size,
186
- etag: row.etag,
187
- uploaded: new Date(row.uploaded),
188
- httpMetadata: JSON.parse(row.http_metadata || "{}"),
189
- customMetadata: JSON.parse(row.custom_metadata || "{}"),
190
- };
191
- },
192
-
193
- async delete(key: string) {
194
- const row = sqlite.prepare("SELECT blob_id FROM _mf_objects WHERE key = ?").get(key) as any;
195
- if (row) {
196
- const filePath = getBlobPath(row.blob_id);
197
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
198
- sqlite.prepare("DELETE FROM _mf_objects WHERE key = ?").run(key);
199
- }
200
- },
201
-
202
- async list() {
203
- const rows = sqlite.prepare("SELECT * FROM _mf_objects").all() as any[];
204
- return {
205
- objects: rows.map((row) => ({
206
- key: row.key,
207
- size: row.size,
208
- etag: row.etag,
209
- uploaded: new Date(row.uploaded),
210
- httpMetadata: JSON.parse(row.http_metadata || "{}"),
211
- customMetadata: JSON.parse(row.custom_metadata || "{}"),
212
- })),
213
- truncated: false,
214
- };
215
- },
216
- };
217
- }
@@ -1,409 +0,0 @@
1
- import { faker } from "@faker-js/faker";
2
- import type { Collection, DatabaseAdapter, Field, OpacaConfig } from "../types";
3
-
4
- /**
5
- * Default generators for each field type.
6
- */
7
- export const defaultFieldGenerators: Record<string, () => any> = {
8
- text: () => faker.lorem.words(3),
9
- textarea: () => faker.lorem.paragraph(),
10
- number: () => faker.number.int({ min: 1, max: 1000 }),
11
- richtext: () =>
12
- JSON.stringify({
13
- root: {
14
- children: [
15
- {
16
- children: [
17
- {
18
- detail: 0,
19
- format: 0,
20
- mode: "normal",
21
- style: "",
22
- text: faker.lorem.sentence(),
23
- type: "text",
24
- version: 1,
25
- },
26
- ],
27
- direction: "ltr",
28
- format: "",
29
- indent: 0,
30
- type: "heading",
31
- tag: "h1",
32
- version: 1,
33
- },
34
- {
35
- children: [
36
- {
37
- detail: 0,
38
- format: 0,
39
- mode: "normal",
40
- style: "",
41
- text: faker.lorem.paragraphs(2),
42
- type: "text",
43
- version: 1,
44
- },
45
- ],
46
- direction: "ltr",
47
- format: "",
48
- indent: 0,
49
- type: "paragraph",
50
- version: 1,
51
- },
52
- ],
53
- direction: "ltr",
54
- format: "",
55
- indent: 0,
56
- type: "root",
57
- version: 1,
58
- },
59
- }),
60
- boolean: () => faker.datatype.boolean(),
61
- date: () => faker.date.recent().toISOString(),
62
- email: () => faker.internet.email(),
63
- json: () => ({ [faker.lorem.word()]: faker.lorem.sentence() }),
64
- select: () => faker.lorem.word(),
65
- radio: () => faker.lorem.word(),
66
- // Add block generator template
67
- blocks: () => [],
68
- };
69
-
70
- async function getRandomIds(
71
- db: DatabaseAdapter,
72
- relationTo: string,
73
- count: number = 1,
74
- ): Promise<any[]> {
75
- try {
76
- const result = await db.find(relationTo, {}, { limit: 50 });
77
- const docs = result.docs || [];
78
-
79
- if (docs.length === 0) {
80
- // Don't log for system collections or if it's expected
81
- if (!relationTo.startsWith("_")) {
82
- console.warn(`[Seeding] No documents found in '${relationTo}' for relationship.`);
83
- }
84
- return [];
85
- }
86
-
87
- // Shuffle and pick
88
- const shuffled = [...docs].sort(() => 0.5 - Math.random());
89
- const selected = shuffled.slice(0, count).map((doc) => (doc as any).id);
90
- return selected;
91
- } catch (e) {
92
- console.error(`[Seeding] Failed to fetch random IDs for ${relationTo}:`, e);
93
- return [];
94
- }
95
- }
96
-
97
- /**
98
- * Resolves a relationship by picking a random ID from the target collection.
99
- */
100
-
101
- /**
102
- * Generates data for a list of fields, recursively handling groups and layouts.
103
- */
104
- async function generateDataForFields(
105
- fields: Field[],
106
- db: DatabaseAdapter,
107
- locales: string[] = [],
108
- ): Promise<Record<string, any>> {
109
- const record: Record<string, any> = {};
110
-
111
- for (const field of fields) {
112
- // 1. Handle Layout Fields (Recursive)
113
- if (field.type === "row" || field.type === "collapsible") {
114
- Object.assign(record, await generateDataForFields(field.fields, db, locales));
115
- continue;
116
- }
117
-
118
- if (field.type === "tabs") {
119
- for (const tab of field.tabs) {
120
- Object.assign(record, await generateDataForFields(tab.fields, db, locales));
121
- }
122
- continue;
123
- }
124
-
125
- // 2. Handle Group (Nested)
126
- if (field.type === "group" && field.name) {
127
- record[field.name] = await generateDataForFields(field.fields, db, locales);
128
- continue;
129
- }
130
-
131
- // 3. Handle Normal Fields with name
132
- if (field.name) {
133
- const isLocalized = !!(field as any).localized && locales.length > 0;
134
-
135
- const generateFieldValue = async () => {
136
- // Handle Relationships
137
- if (field.type === "relationship" && "relationTo" in field) {
138
- if (field.hasMany) {
139
- const count = faker.number.int({ min: 1, max: 3 });
140
- return await getRandomIds(db, field.relationTo, count);
141
- }
142
- const ids = await getRandomIds(db, field.relationTo, 1);
143
- return ids[0] || null;
144
- }
145
-
146
- // Handle Blocks
147
- if (field.type === "blocks" && field.blocks) {
148
- const blockCount = faker.number.int({ min: 1, max: 3 });
149
- const generatedBlocks = [];
150
-
151
- for (let i = 0; i < blockCount; i++) {
152
- const blockType =
153
- field.blocks[faker.number.int({ min: 0, max: field.blocks.length - 1 })];
154
- if (!blockType) continue;
155
-
156
- const blockData = await generateDataForFields(blockType.fields, db, locales);
157
- generatedBlocks.push({
158
- ...blockData,
159
- blockType: blockType.slug,
160
- id: faker.string.uuid(),
161
- });
162
- }
163
-
164
- return generatedBlocks;
165
- }
166
-
167
- // Default Generators
168
- const generator = defaultFieldGenerators[field.type];
169
- if (generator) {
170
- if (field.type === "select" || field.type === "radio") {
171
- const options = (field as any).options;
172
- const choices = options?.choices || [];
173
- if (choices.length > 0) {
174
- const choice = choices[Math.floor(Math.random() * choices.length)];
175
- return typeof choice === "string" ? choice : choice.value;
176
- }
177
- }
178
- return generator();
179
- }
180
- return null;
181
- };
182
-
183
- if (isLocalized) {
184
- const localizedValue: Record<string, any> = {};
185
- for (const locale of locales) {
186
- localizedValue[locale] = await generateFieldValue();
187
- }
188
- record[field.name] = localizedValue;
189
- } else {
190
- record[field.name] = await generateFieldValue();
191
- }
192
- }
193
- }
194
-
195
- return record;
196
- }
197
-
198
- /**
199
- * Generates a single record for a collection.
200
- */
201
- export async function generateRecord(db: DatabaseAdapter, collection: Collection, locales: string[] = []) {
202
- return generateDataForFields(collection.fields as Field[], db, locales);
203
- }
204
-
205
- /**
206
- * Topologically sorts collections based on their dependencies.
207
- */
208
- export function sortCollections(collections: Collection[]): Collection[] {
209
- const sorted: Collection[] = [];
210
- const visited = new Set<string>();
211
- const visiting = new Set<string>();
212
-
213
- const visit = (collection: Collection) => {
214
- if (visited.has(collection.slug)) return;
215
- if (visiting.has(collection.slug)) {
216
- throw new Error(`Circular dependency detected: ${collection.slug}`);
217
- }
218
-
219
- visiting.add(collection.slug);
220
-
221
- // Implicit dependencies from relationships
222
- const deps: string[] = [];
223
-
224
- // Recursive search for relationships
225
- const findDeps = (fields: Field[]) => {
226
- for (const f of fields) {
227
- if (f.type === "relationship") {
228
- deps.push((f as any).relationTo);
229
- } else if (f.type === "blocks" && f.blocks) {
230
- for (const b of f.blocks) {
231
- findDeps(b.fields as Field[]);
232
- }
233
- } else if ("fields" in f && f.fields) {
234
- findDeps(f.fields as Field[]);
235
- } else if ("tabs" in f && f.tabs) {
236
- for (const t of f.tabs) {
237
- findDeps(t.fields as Field[]);
238
- }
239
- }
240
- }
241
- };
242
- findDeps(collection.fields as Field[]);
243
-
244
- for (const depSlug of deps) {
245
- const depColl = collections.find((c) => c.slug === depSlug);
246
- if (depColl) visit(depColl);
247
- }
248
-
249
- visiting.delete(collection.slug);
250
- visited.add(collection.slug);
251
- sorted.push(collection);
252
- };
253
-
254
- for (const collection of collections) {
255
- visit(collection);
256
- }
257
-
258
- return sorted;
259
- }
260
-
261
- /**
262
- * Seeds the database with mock data.
263
- */
264
- export async function autoSeed(
265
- config: OpacaConfig,
266
- countPerCollection = 10,
267
- reset = false,
268
- type: "collections" | "assets" | "all" = "all",
269
- ) {
270
- const { collections, db, globals, storages, serverURL } = config;
271
-
272
- console.log(`๐ŸŒฑ Starting automatic seed (${countPerCollection} records per collection)...`);
273
-
274
- // Include system collections (like _opaca_assets)
275
- const { getSystemCollections } = await import("../db/system-schema.js");
276
- const systemCollections = getSystemCollections().filter((c) => c.slug === "_opaca_assets");
277
- const allCollections = [...systemCollections, ...collections];
278
-
279
- let collectionsToSeed = sortCollections(allCollections);
280
-
281
- if (type === "assets") {
282
- collectionsToSeed = collectionsToSeed.filter((c) => c.slug === "_opaca_assets");
283
- console.log("๐Ÿ“ Seeding only assets...");
284
- } else if (type === "collections") {
285
- collectionsToSeed = collectionsToSeed.filter((c) => c.slug !== "_opaca_assets");
286
- console.log("๐Ÿ“š Seeding only user collections...");
287
- }
288
-
289
- // Connect and ensure adapter knows the schema (pass ALL collections for proper whitelisting)
290
- await db.connect();
291
- await db.migrate(allCollections, globals || []);
292
-
293
- try {
294
- if (reset) {
295
- console.log("๐Ÿงน Resetting data (deleting existing records)...");
296
- // Delete in reverse order to respect foreign key constraints if any
297
- const reversed = [...collectionsToSeed].reverse();
298
- for (const collection of reversed) {
299
- console.log(`Cleaning ${collection.slug}...`);
300
- await db.deleteMany?.(collection.slug, {});
301
- }
302
- }
303
-
304
- const storageAdapter = (storages as any)?.default || storages;
305
-
306
- const locales = config.i18n?.locales || [];
307
-
308
- for (const collection of collectionsToSeed) {
309
- console.log(`Seeding ${collection.slug}...`);
310
-
311
- const isAssetCollection = collection.slug === "_opaca_assets";
312
-
313
- for (let i = 0; i < countPerCollection; i++) {
314
- let data: any;
315
-
316
- if (isAssetCollection) {
317
- // ... (asset logic remains the same)
318
- const id = faker.string.uuid();
319
- const width = faker.number.int({ min: 400, max: 1200 });
320
- const height = faker.number.int({ min: 300, max: 800 });
321
- const color = faker.color.rgb({ prefix: "" });
322
- const textColor = faker.color.rgb({ prefix: "" });
323
-
324
- // Randomize between Picsum and Placehold.co
325
- const usePicsum = Math.random() > 0.5;
326
- let imageUrl = "";
327
-
328
- if (usePicsum) {
329
- const categories = ["nature", "city", "tech", "people", "animals", "architecture"];
330
- const category = faker.helpers.arrayElement(categories);
331
- imageUrl = `https://picsum.photos/seed/${category}-${id}/${width}/${height}`;
332
- } else {
333
- imageUrl = `https://placehold.co/${width}x${height}/${color}/${textColor}.png?text=Seed+${i}`;
334
- }
335
-
336
- // 1. Fetch real image
337
- const res = await fetch(imageUrl);
338
- if (!res.ok) {
339
- throw new Error(`Failed to fetch placeholder image: ${imageUrl}`);
340
- }
341
-
342
- const arrayBuffer = await res.arrayBuffer();
343
- const mime_type = res.headers.get("content-type")?.split(";")[0] || "image/png";
344
-
345
- const extMap: Record<string, string> = {
346
- "image/jpeg": "jpg",
347
- "image/png": "png",
348
- "image/webp": "webp",
349
- "image/gif": "gif",
350
- "image/svg+xml": "svg",
351
- };
352
-
353
- const ext = extMap[mime_type] || "png";
354
-
355
- // 2. Build FileRecord
356
- const fileRecord = {
357
- filename: `seed-image-${i}.${ext}`,
358
- original_filename: `seed-image-${i}.${ext}`,
359
- mime_type,
360
- filesize: arrayBuffer.byteLength,
361
- buffer: new Uint8Array(arrayBuffer),
362
- };
363
-
364
- // 3. Upload via adapter
365
- if (!storageAdapter || typeof storageAdapter.upload !== "function") {
366
- throw new Error(
367
- "Storage adapter is required for seeding assets and must have an 'upload' method.",
368
- );
369
- }
370
-
371
- const uploaded = await storageAdapter.upload(fileRecord, {
372
- generateUniqueName: true,
373
- customMetadata: {
374
- sourceUrl: imageUrl,
375
- },
376
- });
377
-
378
- // 4. Persist metadata real
379
- data = {
380
- id,
381
- key: uploaded.filename,
382
- filename: uploaded.filename,
383
- originalFilename: fileRecord.original_filename,
384
- mimeType: uploaded.mime_type,
385
- filesize: uploaded.filesize,
386
- width,
387
- height,
388
- bucket: "default",
389
- url: uploaded.url,
390
- thumbnailUrl: uploaded.url, // (placeholder)
391
- altText: faker.lorem.sentence(),
392
- };
393
-
394
- // LOG THE FULL URL
395
- const baseURL = serverURL || "http://localhost:8787";
396
- console.log(`[Asset] Source: ${imageUrl}`);
397
- console.log(`[Asset] Seeded: ${baseURL}/api/assets/${id}/view (${uploaded.filename})`);
398
- } else {
399
- data = await generateRecord(db, collection, locales);
400
- }
401
-
402
- await db.create(collection.slug, data);
403
- }
404
- }
405
- console.log("โœ… Seeding completed.");
406
- } finally {
407
- // Keep connection alive for potential reuse in tests
408
- }
409
- }