@dyrected/core 2.1.0 → 2.4.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.
- package/dist/chunk-7GGHK75W.js +1835 -0
- package/dist/chunk-GZODLJ3C.js +1861 -0
- package/dist/index.cjs +252 -7
- package/dist/index.d.cts +64 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +171 -5
- package/dist/server.cjs +79 -2
- package/dist/server.d.cts +12 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createDyrectedApp,
|
|
3
3
|
normalizeConfig
|
|
4
|
-
} from "./chunk-
|
|
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
|
-
-
|
|
112
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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