opacacms 0.2.0 → 0.2.1

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.
@@ -22,15 +22,12 @@ import {
22
22
  // src/db/better-sqlite.ts
23
23
  import fs from "node:fs/promises";
24
24
  import path from "node:path";
25
- import { createRequire } from "node:module";
26
25
  import { CompiledQuery, FileMigrationProvider, Kysely, Migrator, SqliteDialect } from "kysely";
27
- var require2 = createRequire(import.meta.url);
28
- var Database = require2("better-sqlite3");
29
-
30
26
  class BetterSQLiteAdapter extends BaseDatabaseAdapter {
27
+ path;
31
28
  name = "better-sqlite3";
32
29
  _rawDb;
33
- _db;
30
+ _db = null;
34
31
  _collections = [];
35
32
  _globals = [];
36
33
  push;
@@ -40,27 +37,39 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
40
37
  return this._rawDb;
41
38
  }
42
39
  get db() {
40
+ if (!this._db)
41
+ throw new Error("Database not connected. Call connect() first.");
43
42
  return this._db;
44
43
  }
45
44
  constructor(path2, options) {
46
45
  super();
47
- this._rawDb = new Database(path2);
46
+ this.path = path2;
47
+ this.push = options?.push ?? true;
48
+ this.pushDestructive = options?.pushDestructive ?? false;
49
+ this.migrationDir = options?.migrationDir ?? "./migrations";
50
+ }
51
+ async connect() {
52
+ if (this._db)
53
+ return;
54
+ const { createRequire } = await import("node:module");
55
+ const require2 = createRequire(import.meta.url);
56
+ const Database = require2("better-sqlite3");
57
+ this._rawDb = new Database(this.path);
48
58
  this._db = new Kysely({
49
59
  dialect: new SqliteDialect({
50
60
  database: this._rawDb
51
61
  })
52
62
  });
53
- this.push = options?.push ?? true;
54
- this.pushDestructive = options?.pushDestructive ?? false;
55
- this.migrationDir = options?.migrationDir ?? "./migrations";
56
63
  }
57
- async connect() {}
58
64
  async disconnect() {
59
- await this._db.destroy();
65
+ if (this._db)
66
+ await this.db.destroy();
60
67
  }
61
68
  async unsafe(query, params) {
69
+ if (!this._db)
70
+ await this.connect();
62
71
  const compiled = CompiledQuery.raw(query, params || []);
63
- const result = await this._db.executeQuery(compiled);
72
+ const result = await this.db.executeQuery(compiled);
64
73
  return result.rows;
65
74
  }
66
75
  async coerceData(collection, data) {
@@ -104,7 +113,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
104
113
  }
105
114
  async count(collection, query) {
106
115
  const tableName = toSnakeCase(collection);
107
- let qb = this._db.selectFrom(tableName).select((eb) => eb.fn.count("id").as("count"));
116
+ let qb = this.db.selectFrom(tableName).select((eb) => eb.fn.count("id").as("count"));
108
117
  if (query && Object.keys(query).length > 0) {
109
118
  qb = qb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
110
119
  }
@@ -113,7 +122,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
113
122
  }
114
123
  async create(collection, data) {
115
124
  const tableName = toSnakeCase(collection);
116
- return this._db.transaction().execute(async (tx) => {
125
+ return this.db.transaction().execute(async (tx) => {
117
126
  const colDef = this._collections.find((c) => c.slug === collection);
118
127
  const jsonFields = colDef?.fields.filter((f) => ["richtext", "json", "file"].includes(f.type)).map((f) => f.name) || [];
119
128
  const flatData = flattenPayload(data, "", jsonFields);
@@ -194,7 +203,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
194
203
  }
195
204
  async findOne(collection, query, tx) {
196
205
  const tableName = toSnakeCase(collection);
197
- const executor = tx || this._db;
206
+ const executor = tx || this.db;
198
207
  let qb = executor.selectFrom(tableName).selectAll();
199
208
  if (query && Object.keys(query).length > 0) {
200
209
  qb = qb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
@@ -247,7 +256,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
247
256
  const offset = (page - 1) * limit;
248
257
  const total = await this.count(collection, query);
249
258
  const tableName = toSnakeCase(collection);
250
- let qb = this._db.selectFrom(tableName).selectAll().limit(limit).offset(offset);
259
+ let qb = this.db.selectFrom(tableName).selectAll().limit(limit).offset(offset);
251
260
  if (query && Object.keys(query).length > 0) {
252
261
  qb = qb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
253
262
  }
@@ -286,7 +295,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
286
295
  if (field.type === "relationship" && "hasMany" in field && field.hasMany) {
287
296
  const joinTableName = `${toSnakeCase(collection)}_${snakeName}_relations`.toLowerCase();
288
297
  try {
289
- const allRelations = await this._db.selectFrom(joinTableName).selectAll().where("source_id", "in", rowIds).orderBy("order", "asc").execute();
298
+ const allRelations = await this.db.selectFrom(joinTableName).selectAll().where("source_id", "in", rowIds).orderBy("order", "asc").execute();
290
299
  const relationsBySource = allRelations.reduce((acc, r) => {
291
300
  if (!acc[r.source_id])
292
301
  acc[r.source_id] = [];
@@ -310,7 +319,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
310
319
  for (const b of field.blocks) {
311
320
  const blockTableName = `${collection}_${snakeName}_${toSnakeCase(b.slug)}`.toLowerCase();
312
321
  try {
313
- const allBlocks = await this._db.selectFrom(blockTableName).selectAll().where("_parent_id", "in", rowIds).execute();
322
+ const allBlocks = await this.db.selectFrom(blockTableName).selectAll().where("_parent_id", "in", rowIds).execute();
314
323
  for (const blk of allBlocks) {
315
324
  const uf = unflattenRow(blk);
316
325
  uf.blockType = blk.block_type;
@@ -356,7 +365,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
356
365
  }
357
366
  async update(collection, query, data) {
358
367
  const tableName = toSnakeCase(collection);
359
- return this._db.transaction().execute(async (tx) => {
368
+ return this.db.transaction().execute(async (tx) => {
360
369
  let normalizedQuery = query;
361
370
  if (typeof query !== "object" || query === null) {
362
371
  normalizedQuery = { id: query };
@@ -447,7 +456,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
447
456
  const current = await this.findOne(collection, normalizedQuery);
448
457
  if (!current)
449
458
  return false;
450
- await this._db.transaction().execute(async (tx) => {
459
+ await this.db.transaction().execute(async (tx) => {
451
460
  const colDef = this._collections.find((c) => c.slug === collection);
452
461
  if (colDef) {
453
462
  for (const field of colDef.fields) {
@@ -473,7 +482,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
473
482
  }
474
483
  async updateMany(collection, query, data) {
475
484
  const tableName = toSnakeCase(collection);
476
- return this._db.transaction().execute(async (tx) => {
485
+ return this.db.transaction().execute(async (tx) => {
477
486
  let qb = tx.updateTable(tableName);
478
487
  if (query && Object.keys(query).length > 0) {
479
488
  qb = qb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
@@ -502,7 +511,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
502
511
  }
503
512
  async deleteMany(collection, query) {
504
513
  const tableName = toSnakeCase(collection);
505
- return this._db.transaction().execute(async (tx) => {
514
+ return this.db.transaction().execute(async (tx) => {
506
515
  let selectQb = tx.selectFrom(tableName).select("id");
507
516
  if (query && Object.keys(query).length > 0) {
508
517
  selectQb = selectQb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
@@ -536,7 +545,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
536
545
  }
537
546
  async findGlobal(slug) {
538
547
  const tableName = toSnakeCase(slug);
539
- const row = await this._db.selectFrom(tableName).selectAll().limit(1).executeTakeFirst();
548
+ const row = await this.db.selectFrom(tableName).selectAll().limit(1).executeTakeFirst();
540
549
  return row ? unflattenRow(row) : null;
541
550
  }
542
551
  async updateGlobal(slug, data) {
@@ -550,15 +559,15 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
550
559
  if (!flatData.id)
551
560
  flatData.id = "global";
552
561
  flatData[toSnakeCase("createdAt")] = now;
553
- await this._db.insertInto(tableName).values(flatData).execute();
562
+ await this.db.insertInto(tableName).values(flatData).execute();
554
563
  } else {
555
- await this._db.updateTable(tableName).set(flatData).where("id", "=", existing.id).execute();
564
+ await this.db.updateTable(tableName).set(flatData).where("id", "=", existing.id).execute();
556
565
  }
557
566
  return this.findGlobal(slug);
558
567
  }
559
568
  async runMigrations() {
560
569
  const migrator = new Migrator({
561
- db: this._db,
570
+ db: this.db,
562
571
  provider: new FileMigrationProvider({
563
572
  fs,
564
573
  path,
@@ -581,7 +590,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
581
590
  this._collections = collections;
582
591
  this._globals = globals;
583
592
  if (this.push) {
584
- await pushSchema(this._db, "sqlite", collections, globals, {
593
+ await pushSchema(this.db, "sqlite", collections, globals, {
585
594
  pushDestructive: this.pushDestructive
586
595
  });
587
596
  }
@@ -4,7 +4,7 @@ import {
4
4
 
5
5
  // src/cli/commands/seed-command.ts
6
6
  async function seedCommand(opaca, count, reset, type) {
7
- const { autoSeed } = await import("./chunk-swtcpvhf.js");
7
+ const { autoSeed } = await import("./chunk-jdfw4v3r.js");
8
8
  try {
9
9
  await autoSeed(opaca, count, reset, type);
10
10
  } catch (error) {
@@ -0,0 +1,311 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-8sqjbsgt.js";
4
+
5
+ // src/cli/seeding.ts
6
+ import { faker } from "@faker-js/faker";
7
+ var defaultFieldGenerators = {
8
+ text: () => faker.lorem.words(3),
9
+ textarea: () => faker.lorem.paragraph(),
10
+ number: () => faker.number.int({ min: 1, max: 1000 }),
11
+ richtext: () => JSON.stringify({
12
+ root: {
13
+ children: [
14
+ {
15
+ children: [
16
+ {
17
+ detail: 0,
18
+ format: 0,
19
+ mode: "normal",
20
+ style: "",
21
+ text: faker.lorem.sentence(),
22
+ type: "text",
23
+ version: 1
24
+ }
25
+ ],
26
+ direction: "ltr",
27
+ format: "",
28
+ indent: 0,
29
+ type: "heading",
30
+ tag: "h1",
31
+ version: 1
32
+ },
33
+ {
34
+ children: [
35
+ {
36
+ detail: 0,
37
+ format: 0,
38
+ mode: "normal",
39
+ style: "",
40
+ text: faker.lorem.paragraphs(2),
41
+ type: "text",
42
+ version: 1
43
+ }
44
+ ],
45
+ direction: "ltr",
46
+ format: "",
47
+ indent: 0,
48
+ type: "paragraph",
49
+ version: 1
50
+ }
51
+ ],
52
+ direction: "ltr",
53
+ format: "",
54
+ indent: 0,
55
+ type: "root",
56
+ version: 1
57
+ }
58
+ }),
59
+ boolean: () => faker.datatype.boolean(),
60
+ date: () => faker.date.recent().toISOString(),
61
+ email: () => faker.internet.email(),
62
+ json: () => ({ [faker.lorem.word()]: faker.lorem.sentence() }),
63
+ select: () => faker.lorem.word(),
64
+ radio: () => faker.lorem.word(),
65
+ blocks: () => []
66
+ };
67
+ async function getRandomIds(db, relationTo, count = 1) {
68
+ try {
69
+ const result = await db.find(relationTo, {}, { limit: 50 });
70
+ const docs = result.docs || [];
71
+ if (docs.length === 0) {
72
+ if (!relationTo.startsWith("_")) {
73
+ console.warn(`[Seeding] No documents found in '${relationTo}' for relationship.`);
74
+ }
75
+ return [];
76
+ }
77
+ const shuffled = [...docs].sort(() => 0.5 - Math.random());
78
+ const selected = shuffled.slice(0, count).map((doc) => doc.id);
79
+ return selected;
80
+ } catch (e) {
81
+ console.error(`[Seeding] Failed to fetch random IDs for ${relationTo}:`, e);
82
+ return [];
83
+ }
84
+ }
85
+ async function generateDataForFields(fields, db, locales = []) {
86
+ const record = {};
87
+ for (const field of fields) {
88
+ if (field.type === "row" || field.type === "collapsible") {
89
+ Object.assign(record, await generateDataForFields(field.fields, db, locales));
90
+ continue;
91
+ }
92
+ if (field.type === "tabs") {
93
+ for (const tab of field.tabs) {
94
+ Object.assign(record, await generateDataForFields(tab.fields, db, locales));
95
+ }
96
+ continue;
97
+ }
98
+ if (field.type === "group" && field.name) {
99
+ record[field.name] = await generateDataForFields(field.fields, db, locales);
100
+ continue;
101
+ }
102
+ if (field.name) {
103
+ const isLocalized = !!field.localized && locales.length > 0;
104
+ const generateFieldValue = async () => {
105
+ if (field.type === "relationship" && "relationTo" in field) {
106
+ if (field.hasMany) {
107
+ const count = faker.number.int({ min: 1, max: 3 });
108
+ return await getRandomIds(db, field.relationTo, count);
109
+ }
110
+ const ids = await getRandomIds(db, field.relationTo, 1);
111
+ return ids[0] || null;
112
+ }
113
+ if (field.type === "blocks" && field.blocks) {
114
+ const blockCount = faker.number.int({ min: 1, max: 3 });
115
+ const generatedBlocks = [];
116
+ for (let i = 0;i < blockCount; i++) {
117
+ const blockType = field.blocks[faker.number.int({ min: 0, max: field.blocks.length - 1 })];
118
+ if (!blockType)
119
+ continue;
120
+ const blockData = await generateDataForFields(blockType.fields, db, locales);
121
+ generatedBlocks.push({
122
+ ...blockData,
123
+ blockType: blockType.slug,
124
+ id: faker.string.uuid()
125
+ });
126
+ }
127
+ return generatedBlocks;
128
+ }
129
+ const generator = defaultFieldGenerators[field.type];
130
+ if (generator) {
131
+ if (field.type === "select" || field.type === "radio") {
132
+ const options = field.options;
133
+ const choices = options?.choices || [];
134
+ if (choices.length > 0) {
135
+ const choice = choices[Math.floor(Math.random() * choices.length)];
136
+ return typeof choice === "string" ? choice : choice.value;
137
+ }
138
+ }
139
+ return generator();
140
+ }
141
+ return null;
142
+ };
143
+ if (isLocalized) {
144
+ const localizedValue = {};
145
+ for (const locale of locales) {
146
+ localizedValue[locale] = await generateFieldValue();
147
+ }
148
+ record[field.name] = localizedValue;
149
+ } else {
150
+ record[field.name] = await generateFieldValue();
151
+ }
152
+ }
153
+ }
154
+ return record;
155
+ }
156
+ async function generateRecord(db, collection, locales = []) {
157
+ return generateDataForFields(collection.fields, db, locales);
158
+ }
159
+ function sortCollections(collections) {
160
+ const sorted = [];
161
+ const visited = new Set;
162
+ const visiting = new Set;
163
+ const visit = (collection) => {
164
+ if (visited.has(collection.slug))
165
+ return;
166
+ if (visiting.has(collection.slug)) {
167
+ throw new Error(`Circular dependency detected: ${collection.slug}`);
168
+ }
169
+ visiting.add(collection.slug);
170
+ const deps = [];
171
+ const findDeps = (fields) => {
172
+ for (const f of fields) {
173
+ if (f.type === "relationship") {
174
+ deps.push(f.relationTo);
175
+ } else if (f.type === "blocks" && f.blocks) {
176
+ for (const b of f.blocks) {
177
+ findDeps(b.fields);
178
+ }
179
+ } else if ("fields" in f && f.fields) {
180
+ findDeps(f.fields);
181
+ } else if ("tabs" in f && f.tabs) {
182
+ for (const t of f.tabs) {
183
+ findDeps(t.fields);
184
+ }
185
+ }
186
+ }
187
+ };
188
+ findDeps(collection.fields);
189
+ for (const depSlug of deps) {
190
+ const depColl = collections.find((c) => c.slug === depSlug);
191
+ if (depColl)
192
+ visit(depColl);
193
+ }
194
+ visiting.delete(collection.slug);
195
+ visited.add(collection.slug);
196
+ sorted.push(collection);
197
+ };
198
+ for (const collection of collections) {
199
+ visit(collection);
200
+ }
201
+ return sorted;
202
+ }
203
+ async function autoSeed(config, countPerCollection = 10, reset = false, type = "all") {
204
+ const { collections, db, globals, storages, serverURL } = config;
205
+ console.log(`\uD83C\uDF31 Starting automatic seed (${countPerCollection} records per collection)...`);
206
+ const { getSystemCollections } = await import("./chunk-3rdhbedb.js");
207
+ const systemCollections = getSystemCollections().filter((c) => c.slug === "_opaca_assets");
208
+ const allCollections = [...systemCollections, ...collections];
209
+ let collectionsToSeed = sortCollections(allCollections);
210
+ if (type === "assets") {
211
+ collectionsToSeed = collectionsToSeed.filter((c) => c.slug === "_opaca_assets");
212
+ console.log("\uD83D\uDCC1 Seeding only assets...");
213
+ } else if (type === "collections") {
214
+ collectionsToSeed = collectionsToSeed.filter((c) => c.slug !== "_opaca_assets");
215
+ console.log("\uD83D\uDCDA Seeding only user collections...");
216
+ }
217
+ await db.connect();
218
+ await db.migrate(allCollections, globals || []);
219
+ try {
220
+ if (reset) {
221
+ console.log("\uD83E\uDDF9 Resetting data (deleting existing records)...");
222
+ const reversed = [...collectionsToSeed].reverse();
223
+ for (const collection of reversed) {
224
+ console.log(`Cleaning ${collection.slug}...`);
225
+ await db.deleteMany?.(collection.slug, {});
226
+ }
227
+ }
228
+ const storageAdapter = storages?.default || storages;
229
+ const locales = config.i18n?.locales || [];
230
+ for (const collection of collectionsToSeed) {
231
+ console.log(`Seeding ${collection.slug}...`);
232
+ const isAssetCollection = collection.slug === "_opaca_assets";
233
+ for (let i = 0;i < countPerCollection; i++) {
234
+ let data;
235
+ if (isAssetCollection) {
236
+ const id = faker.string.uuid();
237
+ const width = faker.number.int({ min: 400, max: 1200 });
238
+ const height = faker.number.int({ min: 300, max: 800 });
239
+ const color = faker.color.rgb({ prefix: "" });
240
+ const textColor = faker.color.rgb({ prefix: "" });
241
+ const usePicsum = Math.random() > 0.5;
242
+ let imageUrl = "";
243
+ if (usePicsum) {
244
+ const categories = ["nature", "city", "tech", "people", "animals", "architecture"];
245
+ const category = faker.helpers.arrayElement(categories);
246
+ imageUrl = `https://picsum.photos/seed/${category}-${id}/${width}/${height}`;
247
+ } else {
248
+ imageUrl = `https://placehold.co/${width}x${height}/${color}/${textColor}.png?text=Seed+${i}`;
249
+ }
250
+ const res = await fetch(imageUrl);
251
+ if (!res.ok) {
252
+ throw new Error(`Failed to fetch placeholder image: ${imageUrl}`);
253
+ }
254
+ const arrayBuffer = await res.arrayBuffer();
255
+ const mime_type = res.headers.get("content-type")?.split(";")[0] || "image/png";
256
+ const extMap = {
257
+ "image/jpeg": "jpg",
258
+ "image/png": "png",
259
+ "image/webp": "webp",
260
+ "image/gif": "gif",
261
+ "image/svg+xml": "svg"
262
+ };
263
+ const ext = extMap[mime_type] || "png";
264
+ const fileRecord = {
265
+ filename: `seed-image-${i}.${ext}`,
266
+ original_filename: `seed-image-${i}.${ext}`,
267
+ mime_type,
268
+ filesize: arrayBuffer.byteLength,
269
+ buffer: new Uint8Array(arrayBuffer)
270
+ };
271
+ if (!storageAdapter || typeof storageAdapter.upload !== "function") {
272
+ throw new Error("Storage adapter is required for seeding assets and must have an 'upload' method.");
273
+ }
274
+ const uploaded = await storageAdapter.upload(fileRecord, {
275
+ generateUniqueName: true,
276
+ customMetadata: {
277
+ sourceUrl: imageUrl
278
+ }
279
+ });
280
+ data = {
281
+ id,
282
+ key: uploaded.filename,
283
+ filename: uploaded.filename,
284
+ originalFilename: fileRecord.original_filename,
285
+ mimeType: uploaded.mime_type,
286
+ filesize: uploaded.filesize,
287
+ width,
288
+ height,
289
+ bucket: "default",
290
+ url: uploaded.url,
291
+ thumbnailUrl: uploaded.url,
292
+ altText: faker.lorem.sentence()
293
+ };
294
+ const baseURL = serverURL || "http://localhost:8787";
295
+ console.log(`[Asset] Source: ${imageUrl}`);
296
+ console.log(`[Asset] Seeded: ${baseURL}/api/assets/${id}/view (${uploaded.filename})`);
297
+ } else {
298
+ data = await generateRecord(db, collection, locales);
299
+ }
300
+ await db.create(collection.slug, data);
301
+ }
302
+ }
303
+ console.log("✅ Seeding completed.");
304
+ } finally {}
305
+ }
306
+ export {
307
+ sortCollections,
308
+ generateRecord,
309
+ defaultFieldGenerators,
310
+ autoSeed
311
+ };