@dyrected/core 2.1.0 → 2.4.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createDyrectedApp,
3
3
  normalizeConfig
4
- } from "./chunk-22JTWD74.js";
4
+ } from "./chunk-GZODLJ3C.js";
5
5
 
6
6
  // src/utils/setup-prompt.ts
7
7
  function buildEnvironmentSection(frameworkLabel, isSelfHosted, config) {
@@ -99,7 +99,10 @@ function buildConstraintsSection() {
99
99
  - RESILIENCE : If Dyrected backend is unreachable, fall back to
100
100
  initialData and show stale content \u2014 never an error page.
101
101
  All relationship fields must handle null gracefully.
102
- Every block renderer must have a default fallback case.`;
102
+ Every block renderer must have a default fallback case.
103
+ - AUTO-SEEDING : Use initialData: [...] in CollectionConfig or GlobalConfig
104
+ to automatically populate the database on first run.
105
+ This is great for demo content or default settings.`;
103
106
  }
104
107
  function buildSchemaRulesSection() {
105
108
  return `
@@ -108,8 +111,10 @@ function buildSchemaRulesSection() {
108
111
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
109
112
  - Never drop existing fields from the schema. Mark unused fields as deprecated only.
110
113
  - All new fields must have a defaultValue.
111
- - Never rename a field slug \u2014 add a new field and migrate data separately.
112
- - For Cloud deployments, run npx @dyrected/cli sync:schema after every config change. Self-hosted deployments sync automatically on startup.`;
114
+ - Use renameTo: 'oldName' to lazily migrate data when renaming fields.
115
+ - Use promoted: true for fields that need high-performance SQL indexing or unique constraints.
116
+ - For Cloud deployments, run npx @dyrected/cli sync:schema after every config change. Self-hosted deployments sync automatically on startup.
117
+ `;
113
118
  }
114
119
  function buildDoNotSection() {
115
120
  return `
@@ -156,6 +161,8 @@ FIELD OPTIONS:
156
161
  - unique \u2014 database-level uniqueness constraint
157
162
  - hasMany \u2014 allow multiple values (for relationship, select, image)
158
163
  - defaultValue \u2014 fallback value (required on all new fields added to existing schemas)
164
+ - promoted \u2014 extracts field to a real SQL column for native indexing (SQL adapters only)
165
+ - renameTo \u2014 name of the old field key to migrate data from (lazy migration)
159
166
  - admin.condition \u2014 Jexl expression string to conditionally show/hide field
160
167
  e.g. "status == \\"published\\""
161
168
  - admin.readOnly \u2014 display only, not editable
@@ -254,6 +261,7 @@ const settings = defineGlobal({
254
261
  export default defineConfig({
255
262
  collections: [media, pages],
256
263
  globals: [settings],
264
+ // Use SqliteAdapter for local, PostgresAdapter or MySqlAdapter for production
257
265
  db: new SqliteAdapter({ filename: './dyrected.db' }),
258
266
  })
259
267
  \`\`\``;
@@ -815,6 +823,162 @@ function generateAIPrompt(activeTab, config) {
815
823
  return sections.join("\n");
816
824
  }
817
825
 
826
+ // src/utils/parse-where.ts
827
+ function assertNever(op, context) {
828
+ throw new Error(`[dyrected/core] Unhandled where operator "${op}" in ${context}`);
829
+ }
830
+ function parseSqlWhere(where, getJsonField, placeholder = "?") {
831
+ const params = [];
832
+ let pgIndex = 1;
833
+ function next() {
834
+ return placeholder === "pg" ? `$${pgIndex++}` : "?";
835
+ }
836
+ function col(field) {
837
+ return field === "id" ? "id" : getJsonField(field);
838
+ }
839
+ function buildOperator(field, value) {
840
+ const c = col(field);
841
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
842
+ params.push(value);
843
+ return `${c} = ${next()}`;
844
+ }
845
+ const entries = Object.entries(value);
846
+ if (entries.length !== 1) {
847
+ return entries.map(([op2, operand2]) => buildSingleOp(c, op2, operand2)).join(" AND ");
848
+ }
849
+ const [op, operand] = entries[0];
850
+ return buildSingleOp(c, op, operand);
851
+ }
852
+ function buildSingleOp(c, op, operand) {
853
+ switch (op) {
854
+ case "equals":
855
+ params.push(operand);
856
+ return `${c} = ${next()}`;
857
+ case "not_equals":
858
+ params.push(operand);
859
+ return `${c} != ${next()}`;
860
+ case "in": {
861
+ const vals = Array.isArray(operand) ? operand : [operand];
862
+ if (vals.length === 0) return "1=0";
863
+ const placeholders = vals.map((v) => {
864
+ params.push(v);
865
+ return next();
866
+ });
867
+ return `${c} IN (${placeholders.join(", ")})`;
868
+ }
869
+ case "not_in": {
870
+ const vals = Array.isArray(operand) ? operand : [operand];
871
+ if (vals.length === 0) return "1=1";
872
+ const placeholders = vals.map((v) => {
873
+ params.push(v);
874
+ return next();
875
+ });
876
+ return `${c} NOT IN (${placeholders.join(", ")})`;
877
+ }
878
+ case "gt":
879
+ params.push(operand);
880
+ return `${c} > ${next()}`;
881
+ case "gte":
882
+ params.push(operand);
883
+ return `${c} >= ${next()}`;
884
+ case "lt":
885
+ params.push(operand);
886
+ return `${c} < ${next()}`;
887
+ case "lte":
888
+ params.push(operand);
889
+ return `${c} <= ${next()}`;
890
+ case "contains":
891
+ params.push(`%${operand}%`);
892
+ return `${c} LIKE ${next()}`;
893
+ case "starts_with":
894
+ params.push(`${operand}%`);
895
+ return `${c} LIKE ${next()}`;
896
+ case "exists":
897
+ return operand ? `${c} IS NOT NULL` : `${c} IS NULL`;
898
+ default:
899
+ return assertNever(op, "parseSqlWhere");
900
+ }
901
+ }
902
+ function buildClause(w) {
903
+ const parts = [];
904
+ for (const [field, value] of Object.entries(w)) {
905
+ if (field === "OR" && Array.isArray(value)) {
906
+ const sub = value.map((v) => `(${buildClause(v)})`).join(" OR ");
907
+ parts.push(`(${sub})`);
908
+ } else if (field === "AND" && Array.isArray(value)) {
909
+ const sub = value.map((v) => `(${buildClause(v)})`).join(" AND ");
910
+ parts.push(`(${sub})`);
911
+ } else {
912
+ parts.push(buildOperator(field, value));
913
+ }
914
+ }
915
+ return parts.length ? parts.join(" AND ") : "1=1";
916
+ }
917
+ const sql = buildClause(where);
918
+ return { sql, params };
919
+ }
920
+ function parseMongoWhere(where) {
921
+ function buildOperator(field, value) {
922
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
923
+ return { [field]: { $eq: value } };
924
+ }
925
+ const entries = Object.entries(value);
926
+ if (entries.length !== 1) {
927
+ const merged = {};
928
+ for (const [op2, operand2] of entries) {
929
+ Object.assign(merged, buildSingleOp(field, op2, operand2)[field]);
930
+ }
931
+ return { [field]: merged };
932
+ }
933
+ const [op, operand] = entries[0];
934
+ return buildSingleOp(field, op, operand);
935
+ }
936
+ function buildSingleOp(field, op, operand) {
937
+ switch (op) {
938
+ case "equals":
939
+ return { [field]: { $eq: operand } };
940
+ case "not_equals":
941
+ return { [field]: { $ne: operand } };
942
+ case "in":
943
+ return { [field]: { $in: Array.isArray(operand) ? operand : [operand] } };
944
+ case "not_in":
945
+ return { [field]: { $nin: Array.isArray(operand) ? operand : [operand] } };
946
+ case "gt":
947
+ return { [field]: { $gt: operand } };
948
+ case "gte":
949
+ return { [field]: { $gte: operand } };
950
+ case "lt":
951
+ return { [field]: { $lt: operand } };
952
+ case "lte":
953
+ return { [field]: { $lte: operand } };
954
+ case "contains":
955
+ return { [field]: { $regex: operand, $options: "i" } };
956
+ case "starts_with":
957
+ return { [field]: { $regex: `^${operand}`, $options: "i" } };
958
+ case "exists":
959
+ return { [field]: { $exists: operand } };
960
+ default:
961
+ return assertNever(op, "parseMongoWhere");
962
+ }
963
+ }
964
+ function buildClause(w) {
965
+ const conditions = [];
966
+ for (const [field, value] of Object.entries(w)) {
967
+ if (field === "OR" && Array.isArray(value)) {
968
+ conditions.push({ $or: value.map(buildClause) });
969
+ } else if (field === "AND" && Array.isArray(value)) {
970
+ conditions.push({ $and: value.map(buildClause) });
971
+ } else {
972
+ conditions.push(buildOperator(field, value));
973
+ }
974
+ }
975
+ if (conditions.length === 0) return {};
976
+ if (conditions.length === 1) return conditions[0];
977
+ return { $and: conditions };
978
+ }
979
+ return buildClause(where);
980
+ }
981
+
818
982
  // src/index.ts
819
983
  function defineCollection(config) {
820
984
  return config;
@@ -832,5 +996,7 @@ export {
832
996
  defineGlobal,
833
997
  generateAIPrompt,
834
998
  generateFreshSetupPrompt,
835
- normalizeConfig
999
+ normalizeConfig,
1000
+ parseMongoWhere,
1001
+ parseSqlWhere
836
1002
  };
package/dist/server.cjs CHANGED
@@ -154,7 +154,14 @@ var DefaultsService = class {
154
154
  static apply(fields, data = {}) {
155
155
  const result = { ...data || {} };
156
156
  fields.forEach((field) => {
157
- const value = result[field.name];
157
+ let value = result[field.name];
158
+ if ((value === void 0 || value === null) && field.renameTo) {
159
+ const legacyValue = result[field.renameTo];
160
+ if (legacyValue !== void 0 && legacyValue !== null) {
161
+ value = legacyValue;
162
+ result[field.name] = legacyValue;
163
+ }
164
+ }
158
165
  if (value === void 0 || value === null) {
159
166
  if (field.defaultValue !== void 0) {
160
167
  result[field.name] = field.defaultValue;
@@ -233,6 +240,19 @@ var CollectionController = class {
233
240
  sort,
234
241
  where
235
242
  });
243
+ if (result.total === 0 && this.collection.initialData && !where && page === 1) {
244
+ console.log(`[dyrected/core] Auto-seeding collection "${this.collection.slug}" from config.initialData`);
245
+ for (const data of this.collection.initialData) {
246
+ await db.create({ collection: this.collection.slug, data });
247
+ }
248
+ result = await db.find({
249
+ collection: this.collection.slug,
250
+ limit,
251
+ page,
252
+ sort,
253
+ where
254
+ });
255
+ }
236
256
  result.docs = result.docs.map((doc) => DefaultsService.apply(this.collection.fields, doc));
237
257
  if (depth > 0) {
238
258
  const populationService = new PopulationService(db, config.collections);
@@ -385,6 +405,54 @@ var CollectionController = class {
385
405
  }
386
406
  return c.json({ message: "Deleted" });
387
407
  }
408
+ async deleteMany(c) {
409
+ const config = c.get("config");
410
+ const db = config.db;
411
+ if (!db) return c.json({ message: "Database not configured" }, 500);
412
+ const user = c.get("user");
413
+ let ids = [];
414
+ try {
415
+ const body = await c.req.json().catch(() => null);
416
+ if (body?.ids && Array.isArray(body.ids)) {
417
+ ids = body.ids;
418
+ }
419
+ } catch {
420
+ }
421
+ if (!ids.length) {
422
+ const raw = c.req.queries("ids") ?? c.req.queries("ids[]") ?? [];
423
+ ids = raw.filter(Boolean);
424
+ }
425
+ if (!ids.length) return c.json({ message: "No IDs provided" }, 400);
426
+ const deleted = [];
427
+ const failed = [];
428
+ for (const id of ids) {
429
+ try {
430
+ let before = null;
431
+ if (this.collection.audit) {
432
+ before = await db.findOne({ collection: this.collection.slug, id });
433
+ }
434
+ await db.delete({ collection: this.collection.slug, id });
435
+ deleted.push(id);
436
+ if (this.collection.audit) {
437
+ AuditService.log(db, {
438
+ operation: "delete",
439
+ collection: this.collection.slug,
440
+ documentId: id,
441
+ user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
442
+ before,
443
+ after: null
444
+ });
445
+ }
446
+ } catch (err) {
447
+ failed.push({ id, error: err?.message ?? "Unknown error" });
448
+ }
449
+ }
450
+ return c.json({
451
+ message: `Deleted ${deleted.length} document(s)`,
452
+ deleted,
453
+ ...failed.length ? { failed } : {}
454
+ });
455
+ }
388
456
  async seed(c) {
389
457
  const config = c.get("config");
390
458
  const db = config.db;
@@ -419,7 +487,13 @@ var GlobalController = class {
419
487
  const db = config.db;
420
488
  if (!db) return c.json({ message: "Database not configured" }, 500);
421
489
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
422
- const data = await db.getGlobal({ slug: this.global.slug });
490
+ let data = await db.getGlobal({ slug: this.global.slug });
491
+ const isEmpty = !data || Object.keys(data).length === 0;
492
+ if (isEmpty && this.global.initialData) {
493
+ console.log(`[dyrected/core] Auto-seeding global "${this.global.slug}" from config.initialData`);
494
+ await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
495
+ data = this.global.initialData;
496
+ }
423
497
  const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
424
498
  if (depth > 0 && dataWithDefaults) {
425
499
  const populationService = new PopulationService(db, config.collections);
@@ -1634,6 +1708,7 @@ function registerRoutes(app, config) {
1634
1708
  app.get(path, accessGate(collection, "read"), (c) => controller.find(c));
1635
1709
  app.post(path, accessGate(collection, "create"), (c) => controller.create(c));
1636
1710
  app.post(`${path}/media`, accessGate(collection, "create"), (c) => controller.create(c));
1711
+ app.delete(`${path}/delete-many`, accessGate(collection, "delete"), (c) => controller.deleteMany(c));
1637
1712
  app.get(`${path}/:id`, accessGate(collection, "read"), (c) => controller.findOne(c));
1638
1713
  app.patch(`${path}/:id`, accessGate(collection, "update"), (c) => controller.update(c));
1639
1714
  app.delete(`${path}/:id`, accessGate(collection, "delete"), (c) => controller.delete(c));
@@ -1684,8 +1759,10 @@ function registerRoutes(app, config) {
1684
1759
  if (id) {
1685
1760
  if (method === "GET") return controller.findOne(c);
1686
1761
  if (method === "PATCH") return controller.update(c);
1762
+ if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
1687
1763
  if (method === "DELETE") return controller.delete(c);
1688
1764
  if (method === "POST" && id === "media") return controller.create(c);
1765
+ if (method === "POST" && id === "seed") return controller.seed(c);
1689
1766
  } else {
1690
1767
  if (method === "GET") return controller.find(c);
1691
1768
  if (method === "POST") return controller.create(c);
package/dist/server.d.cts CHANGED
@@ -143,6 +143,18 @@ declare class CollectionController {
143
143
  delete(c: Context<DyrectedContext>): Promise<Response & hono.TypedResponse<{
144
144
  message: string;
145
145
  }, hono_utils_http_status.ContentfulStatusCode, "json">>;
146
+ deleteMany(c: Context<DyrectedContext>): Promise<(Response & hono.TypedResponse<{
147
+ message: string;
148
+ }, 500, "json">) | (Response & hono.TypedResponse<{
149
+ message: string;
150
+ }, 400, "json">) | (Response & hono.TypedResponse<{
151
+ failed?: {
152
+ id: string;
153
+ error: string;
154
+ }[] | undefined;
155
+ message: string;
156
+ deleted: string[];
157
+ }, hono_utils_http_status.ContentfulStatusCode, "json">)>;
146
158
  seed(c: Context<DyrectedContext>): Promise<Response & hono.TypedResponse<{
147
159
  message: string;
148
160
  }, hono_utils_http_status.ContentfulStatusCode, "json">>;
package/dist/server.d.ts CHANGED
@@ -143,6 +143,18 @@ declare class CollectionController {
143
143
  delete(c: Context<DyrectedContext>): Promise<Response & hono.TypedResponse<{
144
144
  message: string;
145
145
  }, hono_utils_http_status.ContentfulStatusCode, "json">>;
146
+ deleteMany(c: Context<DyrectedContext>): Promise<(Response & hono.TypedResponse<{
147
+ message: string;
148
+ }, 500, "json">) | (Response & hono.TypedResponse<{
149
+ message: string;
150
+ }, 400, "json">) | (Response & hono.TypedResponse<{
151
+ failed?: {
152
+ id: string;
153
+ error: string;
154
+ }[] | undefined;
155
+ message: string;
156
+ deleted: string[];
157
+ }, hono_utils_http_status.ContentfulStatusCode, "json">)>;
146
158
  seed(c: Context<DyrectedContext>): Promise<Response & hono.TypedResponse<{
147
159
  message: string;
148
160
  }, hono_utils_http_status.ContentfulStatusCode, "json">>;
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  PreviewController,
7
7
  createDyrectedApp,
8
8
  registerRoutes
9
- } from "./chunk-22JTWD74.js";
9
+ } from "./chunk-GZODLJ3C.js";
10
10
  export {
11
11
  AuthController,
12
12
  CollectionController,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyrected/core",
3
- "version": "2.1.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",