@dyrected/core 2.5.60 → 2.5.62

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 (48) hide show
  1. package/dist/app-config-9hs8DRED.d.cts +1718 -0
  2. package/dist/app-config-9hs8DRED.d.ts +1718 -0
  3. package/dist/app-config-BcuZIpvL.d.cts +1951 -0
  4. package/dist/app-config-BcuZIpvL.d.ts +1951 -0
  5. package/dist/app-config-Bm9-2Am6.d.cts +1875 -0
  6. package/dist/app-config-Bm9-2Am6.d.ts +1875 -0
  7. package/dist/app-config-C0uxEU7Q.d.cts +1874 -0
  8. package/dist/app-config-C0uxEU7Q.d.ts +1874 -0
  9. package/dist/app-config-CBOn8IyZ.d.cts +1838 -0
  10. package/dist/app-config-CBOn8IyZ.d.ts +1838 -0
  11. package/dist/app-config-CJAGGPrk.d.cts +1900 -0
  12. package/dist/app-config-CJAGGPrk.d.ts +1900 -0
  13. package/dist/app-config-CiEDJm0e.d.cts +1912 -0
  14. package/dist/app-config-CiEDJm0e.d.ts +1912 -0
  15. package/dist/app-config-CwaU1de2.d.cts +1868 -0
  16. package/dist/app-config-CwaU1de2.d.ts +1868 -0
  17. package/dist/app-config-DCDh8Gbx.d.cts +1926 -0
  18. package/dist/app-config-DCDh8Gbx.d.ts +1926 -0
  19. package/dist/app-config-DVdSospO.d.cts +1842 -0
  20. package/dist/app-config-DVdSospO.d.ts +1842 -0
  21. package/dist/app-config-Dv5XACR4.d.cts +2065 -0
  22. package/dist/app-config-Dv5XACR4.d.ts +2065 -0
  23. package/dist/app-config-_kkj71CB.d.cts +2010 -0
  24. package/dist/app-config-_kkj71CB.d.ts +2010 -0
  25. package/dist/app-config-ouBRb6Bf.d.cts +1716 -0
  26. package/dist/app-config-ouBRb6Bf.d.ts +1716 -0
  27. package/dist/app-config-tITj_0sn.d.cts +1926 -0
  28. package/dist/app-config-tITj_0sn.d.ts +1926 -0
  29. package/dist/chunk-35NM2WPO.js +1658 -0
  30. package/dist/chunk-57FNM42D.js +2392 -0
  31. package/dist/chunk-BAMX7YUC.js +1815 -0
  32. package/dist/chunk-BQV3QW3Y.js +2588 -0
  33. package/dist/chunk-CUOPCOU2.js +1644 -0
  34. package/dist/chunk-EH3MJGB5.js +2571 -0
  35. package/dist/chunk-M3HKRN7E.js +1665 -0
  36. package/dist/chunk-MQZH7RQC.js +1667 -0
  37. package/dist/chunk-VCYYBN5J.js +1873 -0
  38. package/dist/chunk-WVD7PORQ.js +1672 -0
  39. package/dist/chunk-XZLIBQSO.js +2397 -0
  40. package/dist/index.cjs +404 -44
  41. package/dist/index.d.cts +77 -5
  42. package/dist/index.d.ts +77 -5
  43. package/dist/index.js +37 -4
  44. package/dist/server.cjs +2973 -708
  45. package/dist/server.d.cts +124 -12
  46. package/dist/server.d.ts +124 -12
  47. package/dist/server.js +2066 -695
  48. package/package.json +10 -1
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -24,6 +34,7 @@ __export(index_exports, {
24
34
  LIFECYCLE_EVENT_NAMES: () => LIFECYCLE_EVENT_NAMES,
25
35
  WORKFLOW_HISTORY_COLLECTION: () => WORKFLOW_HISTORY_COLLECTION,
26
36
  availableWorkflowTransitions: () => availableWorkflowTransitions,
37
+ buildSqlOrderBy: () => buildSqlOrderBy,
27
38
  canViewWorkflowDraft: () => canViewWorkflowDraft,
28
39
  createLifecycleEvent: () => createLifecycleEvent,
29
40
  createWorkflowDocument: () => createWorkflowDocument,
@@ -45,6 +56,7 @@ __export(index_exports, {
45
56
  defineMultiSelectField: () => defineMultiSelectField,
46
57
  defineNumberField: () => defineNumberField,
47
58
  defineObjectField: () => defineObjectField,
59
+ definePublishingWorkflow: () => definePublishingWorkflow,
48
60
  defineRadioField: () => defineRadioField,
49
61
  defineRelationshipField: () => defineRelationshipField,
50
62
  defineRichTextField: () => defineRichTextField,
@@ -65,6 +77,7 @@ __export(index_exports, {
65
77
  initializeWorkflowDocument: () => initializeWorkflowDocument,
66
78
  materializeWorkflowDocument: () => materializeWorkflowDocument,
67
79
  normalizeConfig: () => normalizeConfig,
80
+ numericFieldNames: () => numericFieldNames,
68
81
  parseMongoWhere: () => parseMongoWhere,
69
82
  parseSort: () => parseSort,
70
83
  parseSqlWhere: () => parseSqlWhere,
@@ -72,6 +85,7 @@ __export(index_exports, {
72
85
  resolveAdminAuthCollection: () => resolveAdminAuthCollection,
73
86
  runCollectionHooks: () => runCollectionHooks,
74
87
  saveWorkflowDraft: () => saveWorkflowDraft,
88
+ simplePublishingWorkflow: () => simplePublishingWorkflow,
75
89
  transitionWorkflow: () => transitionWorkflow,
76
90
  workflowCapabilities: () => workflowCapabilities
77
91
  });
@@ -85,10 +99,47 @@ var LIFECYCLE_EVENT_NAMES = [
85
99
  "entry.unpublished"
86
100
  ];
87
101
 
102
+ // src/auth/token.ts
103
+ var import_jose = require("jose");
104
+ var import_node_util = require("util");
105
+
106
+ // src/auth/sessions.ts
107
+ var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
108
+
109
+ // src/observability.ts
110
+ var import_api = require("@opentelemetry/api");
111
+ var import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
112
+ var import_exporter_prometheus = require("@opentelemetry/exporter-prometheus");
113
+ var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
114
+ var import_resources = require("@opentelemetry/resources");
115
+ var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
116
+ var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
117
+ var import_pino = __toESM(require("pino"), 1);
118
+ var import_pino_pretty = __toESM(require("pino-pretty"), 1);
119
+ var import_node_stream = require("stream");
120
+ var runtimeByConfig = /* @__PURE__ */ new WeakMap();
121
+ var fallbackLogger = (0, import_pino.default)({
122
+ name: "dyrected",
123
+ enabled: process.env.DISABLE_LOGGING !== "true",
124
+ timestamp: import_pino.stdTimeFunctions.isoTime
125
+ });
126
+ function getObservabilityRuntime(config) {
127
+ if (!config || typeof config !== "object") return void 0;
128
+ return runtimeByConfig.get(config);
129
+ }
130
+ function getConfigLogger(config, component) {
131
+ const base = config?.logger && !("options" in config.logger) ? config.logger : fallbackLogger;
132
+ return base.child({ component });
133
+ }
134
+
88
135
  // src/workflows.ts
89
136
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
90
137
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
91
- function publishingWorkflow() {
138
+ var EDITOR_CAPABILITIES = ["entry.edit", "entry.submit"];
139
+ var PUBLISHER_CAPABILITIES = ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"];
140
+ function definePublishingWorkflow(options = {}) {
141
+ const editors = options.editors ?? ["editor"];
142
+ const publishers = options.publishers ?? ["publisher", "admin"];
92
143
  return {
93
144
  initialState: "draft",
94
145
  draftState: "draft",
@@ -104,10 +155,29 @@ function publishingWorkflow() {
104
155
  { name: "unpublish", label: "Unpublish", from: "published", to: "draft", requiredCapabilities: ["entry.unpublish"], unpublish: true }
105
156
  ],
106
157
  roles: [
107
- { role: "editor", capabilities: ["entry.edit", "entry.submit"] },
108
- { role: "publisher", capabilities: ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"] },
109
- { role: "admin", capabilities: ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"] }
158
+ ...editors.map((role) => ({ role, capabilities: [...EDITOR_CAPABILITIES] })),
159
+ ...publishers.map((role) => ({ role, capabilities: [...PUBLISHER_CAPABILITIES] }))
160
+ ]
161
+ };
162
+ }
163
+ function publishingWorkflow() {
164
+ return definePublishingWorkflow();
165
+ }
166
+ function simplePublishingWorkflow() {
167
+ return {
168
+ initialState: "draft",
169
+ draftState: "draft",
170
+ states: [
171
+ { name: "draft", label: "Draft", color: "neutral" },
172
+ { name: "published", label: "Published", color: "success", published: true }
173
+ ],
174
+ transitions: [
175
+ { name: "publish", label: "Publish", from: "draft", to: "published" },
176
+ { name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
110
177
  ]
178
+ // Intentionally no `roles`: this lightweight workflow (synthesized from
179
+ // `drafts: true`) does no capability-based gating. Draft visibility for a
180
+ // no-roles workflow is handled in `canViewWorkflowDraft`.
111
181
  };
112
182
  }
113
183
  function publicMetadata(meta) {
@@ -126,9 +196,13 @@ function canViewWorkflowDraft(workflow, user) {
126
196
  if (!user) return false;
127
197
  const capabilities = workflowCapabilities(workflow, user);
128
198
  if (capabilities.has("entry.edit")) return true;
129
- return workflow.transitions.some(
199
+ if (workflow.transitions.some(
130
200
  (transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
131
- );
201
+ )) {
202
+ return true;
203
+ }
204
+ if (!workflow.roles || workflow.roles.length === 0) return true;
205
+ return false;
132
206
  }
133
207
  function availableWorkflowTransitions(workflow, state, user) {
134
208
  const capabilities = workflowCapabilities(workflow, user);
@@ -143,9 +217,23 @@ function initializeWorkflowDocument(data, workflow) {
143
217
  __workflow: { state: workflow.initialState, revision: 1 }
144
218
  };
145
219
  }
220
+ function publishedStateName(workflow) {
221
+ return workflow.states.find((state) => state.published)?.name ?? workflow.states[workflow.states.length - 1]?.name ?? workflow.initialState;
222
+ }
146
223
  function materializeWorkflowDocument(doc, workflow, user) {
147
224
  const meta = doc.__workflow;
148
- if (!meta) return doc;
225
+ if (!meta) {
226
+ const { __published: _legacyPublished, __workflow: _legacyWorkflow, ...working2 } = doc;
227
+ const state = publishedStateName(workflow);
228
+ return {
229
+ ...working2,
230
+ _workflow: {
231
+ state,
232
+ revision: 1,
233
+ availableTransitions: availableWorkflowTransitions(workflow, state, user).map((item) => item.name)
234
+ }
235
+ };
236
+ }
149
237
  const { __published, __workflow, ...working } = doc;
150
238
  if (!canViewWorkflowDraft(workflow, user)) {
151
239
  if (!__published || typeof __published !== "object") return null;
@@ -340,7 +428,17 @@ async function transitionWorkflow(args) {
340
428
  try {
341
429
  await hook({ ...hookContext, doc: updated, event: events[0] });
342
430
  } catch (error) {
343
- console.error("[dyrected/workflow] afterTransition hook failed:", error);
431
+ getObservabilityRuntime(config)?.recordWorkflowHookFailure({
432
+ collection: collection.slug,
433
+ transition: transition.name
434
+ });
435
+ getConfigLogger(config, "workflow").error({
436
+ err: error,
437
+ msg: "afterTransition hook failed",
438
+ collection: collection.slug,
439
+ transition: transition.name,
440
+ documentId: id
441
+ });
344
442
  }
345
443
  }
346
444
  return updated;
@@ -381,6 +479,98 @@ function humanizeProviderName(id, type) {
381
479
  return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
382
480
  }
383
481
 
482
+ // src/utils/block-references.ts
483
+ function mergeUniqueBlocks(blocks) {
484
+ const seen = /* @__PURE__ */ new Map();
485
+ for (const block of blocks) {
486
+ const existing = seen.get(block.slug);
487
+ if (existing && existing !== block) {
488
+ throw new Error(
489
+ `Duplicate block slug "${block.slug}" found in the reusable block registry. Block slugs must be unique.`
490
+ );
491
+ }
492
+ if (!existing) seen.set(block.slug, block);
493
+ }
494
+ return Array.from(seen.values());
495
+ }
496
+ function buildRegistry(blocks) {
497
+ return new Map(blocks.map((block) => [block.slug, block]));
498
+ }
499
+ function resolveField(field, registry, blockCache) {
500
+ const next = { ...field };
501
+ if (next.fields) {
502
+ next.fields = next.fields.map(
503
+ (child) => resolveField(child, registry, blockCache)
504
+ );
505
+ }
506
+ if (next.type !== "blocks") return next;
507
+ const hasInlineBlocks = Array.isArray(next.blocks) && next.blocks.length > 0;
508
+ const hasReferences = Array.isArray(next.blockReferences) && next.blockReferences.length > 0;
509
+ if (hasInlineBlocks && hasReferences) {
510
+ throw new Error(
511
+ `Blocks field "${next.name ?? "(unnamed)"}" cannot define both "blocks" and "blockReferences". Use one or the other.`
512
+ );
513
+ }
514
+ if (hasReferences) {
515
+ next.blocks = next.blockReferences.map((slug) => {
516
+ const block = registry.get(slug);
517
+ if (!block) {
518
+ throw new Error(
519
+ `Unknown block reference "${slug}" on blocks field "${next.name ?? "(unnamed)"}". Add it to defineConfig({ blocks: [...] }).`
520
+ );
521
+ }
522
+ return resolveBlock(block, registry, blockCache);
523
+ });
524
+ return next;
525
+ }
526
+ if (hasInlineBlocks) {
527
+ next.blocks = next.blocks.map(
528
+ (block) => resolveBlock(block, registry, blockCache)
529
+ );
530
+ }
531
+ return next;
532
+ }
533
+ function resolveBlock(block, registry, blockCache) {
534
+ const cached = blockCache.get(block);
535
+ if (cached) return cached;
536
+ const resolved = {
537
+ ...block,
538
+ fields: []
539
+ };
540
+ blockCache.set(block, resolved);
541
+ resolved.fields = block.fields.map(
542
+ (field) => resolveField(field, registry, blockCache)
543
+ );
544
+ return resolved;
545
+ }
546
+ function resolveFields(fields, registry, blockCache) {
547
+ return fields.map((field) => resolveField(field, registry, blockCache));
548
+ }
549
+ function normalizeSchemaFragment(fragment) {
550
+ const reusableBlocks = mergeUniqueBlocks(fragment.blocks ?? []);
551
+ const registry = buildRegistry(reusableBlocks);
552
+ const blockCache = /* @__PURE__ */ new WeakMap();
553
+ const resolvedBlocks = reusableBlocks.map(
554
+ (block) => resolveBlock(block, registry, blockCache)
555
+ );
556
+ const resolvedCollections = (fragment.collections ?? []).map(
557
+ (collection) => ({
558
+ ...collection,
559
+ fields: resolveFields(collection.fields ?? [], registry, blockCache)
560
+ })
561
+ );
562
+ const resolvedGlobals = (fragment.globals ?? []).map((global) => ({
563
+ ...global,
564
+ fields: resolveFields(global.fields ?? [], registry, blockCache)
565
+ }));
566
+ return {
567
+ ...fragment,
568
+ ...fragment.blocks ? { blocks: resolvedBlocks } : {},
569
+ ...fragment.collections ? { collections: resolvedCollections } : {},
570
+ ...fragment.globals ? { globals: resolvedGlobals } : {}
571
+ };
572
+ }
573
+
384
574
  // src/utils/config.ts
385
575
  var AUDIT_COLLECTION_SLUG = "__audit";
386
576
  var SYSTEM_FIELDS = [
@@ -415,12 +605,23 @@ var AUDIT_COLLECTION = {
415
605
  fields: [
416
606
  { name: "collection", type: "text", label: "Collection", required: true },
417
607
  { name: "documentId", type: "text", label: "Document ID" },
418
- { name: "operation", type: "select", label: "Operation", options: ["create", "update", "delete"], required: true },
608
+ {
609
+ name: "operation",
610
+ type: "select",
611
+ label: "Operation",
612
+ options: ["create", "update", "delete"],
613
+ required: true
614
+ },
419
615
  { name: "user", type: "text", label: "User ID" },
420
616
  { name: "timestamp", type: "date", label: "Timestamp", required: true },
421
617
  { name: "changes", type: "json", label: "Changes" }
422
618
  ],
423
- access: { read: () => false, create: () => false, update: () => false, delete: () => false },
619
+ access: {
620
+ read: () => false,
621
+ create: () => false,
622
+ update: () => false,
623
+ delete: () => false
624
+ },
424
625
  admin: { hidden: true }
425
626
  };
426
627
  var WORKFLOW_HISTORY_COLLECTION_CONFIG = {
@@ -437,7 +638,12 @@ var WORKFLOW_HISTORY_COLLECTION_CONFIG = {
437
638
  { name: "actorId", type: "text" },
438
639
  { name: "createdAt", type: "date", required: true }
439
640
  ],
440
- access: { read: ({ user }) => !!user, create: () => false, update: () => false, delete: () => false },
641
+ access: {
642
+ read: ({ user }) => !!user,
643
+ create: () => false,
644
+ update: () => false,
645
+ delete: () => false
646
+ },
441
647
  admin: { hidden: true }
442
648
  };
443
649
  var LIFECYCLE_EVENTS_COLLECTION_CONFIG = {
@@ -451,22 +657,56 @@ var LIFECYCLE_EVENTS_COLLECTION_CONFIG = {
451
657
  { name: "actorId", type: "text" },
452
658
  { name: "payload", type: "json", required: true },
453
659
  { name: "attempts", type: "number", required: true },
454
- { name: "status", type: "select", options: ["pending", "processing", "delivered", "failed"], required: true },
660
+ {
661
+ name: "status",
662
+ type: "select",
663
+ options: ["pending", "processing", "delivered", "failed"],
664
+ required: true
665
+ },
455
666
  { name: "nextAttemptAt", type: "date" },
456
667
  { name: "deliveredAt", type: "date" },
457
668
  { name: "lastError", type: "textarea" }
458
669
  ],
459
- access: { read: ({ user }) => !!user?.roles?.includes("admin"), create: () => false, update: () => false, delete: () => false },
670
+ access: {
671
+ read: ({ user }) => !!user?.roles?.includes("admin"),
672
+ create: () => false,
673
+ update: () => false,
674
+ delete: () => false
675
+ },
676
+ admin: { hidden: true }
677
+ };
678
+ var AUTH_SESSIONS_COLLECTION_CONFIG = {
679
+ slug: AUTH_SESSIONS_COLLECTION,
680
+ labels: { singular: "Auth session", plural: "Auth sessions" },
681
+ fields: [
682
+ { name: "userId", type: "text", required: true },
683
+ { name: "email", type: "email", required: true },
684
+ { name: "collection", type: "text", required: true },
685
+ { name: "authSource", type: "text" },
686
+ { name: "providerId", type: "text" },
687
+ { name: "lastIp", type: "text" },
688
+ { name: "lastSeenAt", type: "date" },
689
+ { name: "expiresAt", type: "date" },
690
+ { name: "revokedAt", type: "date" }
691
+ ],
692
+ access: {
693
+ read: () => false,
694
+ create: () => false,
695
+ update: () => false,
696
+ delete: () => false
697
+ },
460
698
  admin: { hidden: true }
461
699
  };
462
700
  function normalizeConfig(config) {
463
- const collections = config?.collections || [];
464
- const globals = config?.globals || [];
701
+ const schemaAwareConfig = normalizeSchemaFragment(config);
702
+ const collections = schemaAwareConfig?.collections || [];
703
+ const globals = schemaAwareConfig?.globals || [];
465
704
  const needsAudit = collections.some((col) => col.audit);
466
- const needsWorkflow = collections.some((col) => col.workflow);
705
+ const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
706
+ const needsAuthSessions = collections.some((col) => !!col.auth);
467
707
  const adminAuthCollectionSlug = getAdminAuthCollection({
468
708
  collections,
469
- adminAuth: config.adminAuth
709
+ adminAuth: schemaAwareConfig.adminAuth
470
710
  })?.slug;
471
711
  const normalizedCollections = collections.map((col) => {
472
712
  let fields = col.fields || [];
@@ -563,7 +803,7 @@ function normalizeConfig(config) {
563
803
  return field;
564
804
  });
565
805
  }
566
- if (adminAuthCollectionSlug && col.slug === adminAuthCollectionSlug && config.adminAuth?.mode === "external") {
806
+ if (adminAuthCollectionSlug && col.slug === adminAuthCollectionSlug && schemaAwareConfig.adminAuth?.mode === "external") {
567
807
  const externalAdminFields = [
568
808
  {
569
809
  name: "authProvider",
@@ -597,23 +837,37 @@ function normalizeConfig(config) {
597
837
  }
598
838
  }
599
839
  const updatedFieldNames = new Set(fields.map((f) => f.name));
600
- const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
840
+ const fieldsToInject = SYSTEM_FIELDS.filter(
841
+ (f) => !updatedFieldNames.has(f.name)
842
+ );
843
+ const workflow = col.workflow || (col.drafts ? simplePublishingWorkflow() : void 0);
601
844
  return {
602
845
  ...col,
846
+ workflow,
603
847
  fields: [...fields, ...fieldsToInject]
604
848
  };
605
849
  });
606
- const hasAuditCollection = normalizedCollections.some((col) => col.slug === AUDIT_COLLECTION_SLUG);
850
+ const hasAuditCollection = normalizedCollections.some(
851
+ (col) => col.slug === AUDIT_COLLECTION_SLUG
852
+ );
607
853
  const systemCollections = [];
608
- if (needsAudit && !hasAuditCollection) systemCollections.push(AUDIT_COLLECTION);
609
- if (needsWorkflow && !normalizedCollections.some((col) => col.slug === WORKFLOW_HISTORY_COLLECTION)) {
854
+ if (needsAudit && !hasAuditCollection)
855
+ systemCollections.push(AUDIT_COLLECTION);
856
+ if (needsWorkflow && !normalizedCollections.some(
857
+ (col) => col.slug === WORKFLOW_HISTORY_COLLECTION
858
+ )) {
610
859
  systemCollections.push(WORKFLOW_HISTORY_COLLECTION_CONFIG);
611
860
  }
612
- if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
861
+ if (needsWorkflow && !normalizedCollections.some(
862
+ (col) => col.slug === LIFECYCLE_EVENTS_COLLECTION
863
+ )) {
613
864
  systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
614
865
  }
866
+ if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
867
+ systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
868
+ }
615
869
  return {
616
- ...config,
870
+ ...schemaAwareConfig,
617
871
  collections: [...normalizedCollections, ...systemCollections],
618
872
  globals
619
873
  };
@@ -623,7 +877,7 @@ function normalizeConfig(config) {
623
877
  function assertNever(op, context) {
624
878
  throw new Error(`[dyrected/core] Unhandled where operator "${op}" in ${context}`);
625
879
  }
626
- function parseSqlWhere(where, getJsonField, placeholder = "?") {
880
+ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "LIKE") {
627
881
  const params = [];
628
882
  let pgIndex = 1;
629
883
  function next() {
@@ -685,10 +939,10 @@ function parseSqlWhere(where, getJsonField, placeholder = "?") {
685
939
  return `${c} <= ${next()}`;
686
940
  case "contains":
687
941
  params.push(`%${operand}%`);
688
- return `${c} LIKE ${next()}`;
942
+ return `${c} ${likeOperator} ${next()}`;
689
943
  case "starts_with":
690
944
  params.push(`${operand}%`);
691
- return `${c} LIKE ${next()}`;
945
+ return `${c} ${likeOperator} ${next()}`;
692
946
  case "exists":
693
947
  return operand ? `${c} IS NOT NULL` : `${c} IS NULL`;
694
948
  default:
@@ -793,6 +1047,33 @@ function parseSort(sort, fallback = { field: "createdAt", direction: "DESC" }) {
793
1047
  }).filter((clause) => Boolean(clause));
794
1048
  return clauses.length > 0 ? clauses : [fallback];
795
1049
  }
1050
+ function numericFieldNames(fields) {
1051
+ const names = /* @__PURE__ */ new Set();
1052
+ for (const field of fields ?? []) {
1053
+ if (field.type === "number" && "name" in field && field.name) {
1054
+ names.add(field.name);
1055
+ }
1056
+ }
1057
+ return names;
1058
+ }
1059
+ var TIMESTAMP_COLUMNS = {
1060
+ createdat: "created_at",
1061
+ created_at: "created_at",
1062
+ updatedat: "updated_at",
1063
+ updated_at: "updated_at"
1064
+ };
1065
+ function buildSqlOrderBy(sort, existingColumns, numericFields, dialect) {
1066
+ return parseSort(sort).map(({ field, direction }) => {
1067
+ const timestamp = TIMESTAMP_COLUMNS[field.toLowerCase()];
1068
+ if (timestamp) return `${dialect.quoteIdent(timestamp)} ${direction}`;
1069
+ if (existingColumns.includes(field) && field !== "id" && field !== "data") {
1070
+ return `${dialect.quoteIdent(field)} ${direction}`;
1071
+ }
1072
+ const extracted = dialect.jsonExtract(field);
1073
+ const expr = numericFields.has(field) ? dialect.castNumeric(extracted) : extracted;
1074
+ return `${expr} ${direction}`;
1075
+ }).join(", ");
1076
+ }
796
1077
 
797
1078
  // src/utils/hooks.ts
798
1079
  async function runCollectionHooks(hooks, args, options = {}) {
@@ -812,7 +1093,10 @@ async function runCollectionHooks(hooks, args, options = {}) {
812
1093
  }
813
1094
  } catch (err) {
814
1095
  if (options.isolated) {
815
- console.error("[dyrected/core] Side-effect hook failed (error isolated \u2014 DB write was successful):", err);
1096
+ getConfigLogger(void 0, "hooks").error({
1097
+ err,
1098
+ msg: "Side-effect hook failed after a successful write"
1099
+ });
816
1100
  } else {
817
1101
  throw err;
818
1102
  }
@@ -857,7 +1141,13 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user, db) {
857
1141
  const item = updatedValue[i];
858
1142
  const origItem = Array.isArray(origValue) ? origValue[i] : null;
859
1143
  arrayResult.push(
860
- await executeFieldBeforeChange(field.fields, item, origItem, user, db)
1144
+ await executeFieldBeforeChange(
1145
+ field.fields,
1146
+ item,
1147
+ origItem,
1148
+ user,
1149
+ db
1150
+ )
861
1151
  );
862
1152
  }
863
1153
  result[field.name] = arrayResult;
@@ -998,7 +1288,15 @@ function generateOpenApi(config) {
998
1288
  },
999
1289
  WorkflowHistoryEntry: {
1000
1290
  type: "object",
1001
- required: ["collection", "documentId", "transition", "from", "to", "revision", "createdAt"],
1291
+ required: [
1292
+ "collection",
1293
+ "documentId",
1294
+ "transition",
1295
+ "from",
1296
+ "to",
1297
+ "revision",
1298
+ "createdAt"
1299
+ ],
1002
1300
  properties: {
1003
1301
  id: { type: "string" },
1004
1302
  collection: { type: "string" },
@@ -1023,7 +1321,11 @@ function generateOpenApi(config) {
1023
1321
  user: { type: "string", nullable: true },
1024
1322
  timestamp: { type: "string", format: "date-time" },
1025
1323
  changes: {
1026
- oneOf: [{ type: "string" }, { type: "object", additionalProperties: true }, { type: "null" }]
1324
+ oneOf: [
1325
+ { type: "string" },
1326
+ { type: "object", additionalProperties: true },
1327
+ { type: "null" }
1328
+ ]
1027
1329
  }
1028
1330
  }
1029
1331
  },
@@ -1115,13 +1417,17 @@ function generateOpenApi(config) {
1115
1417
  get: {
1116
1418
  tags: ["Preferences"],
1117
1419
  summary: "Get an authenticated user preference",
1118
- parameters: [{ name: "key", in: "path", required: true, schema: { type: "string" } }],
1420
+ parameters: [
1421
+ { name: "key", in: "path", required: true, schema: { type: "string" } }
1422
+ ],
1119
1423
  responses: { 200: { description: "Preference value" } }
1120
1424
  },
1121
1425
  put: {
1122
1426
  tags: ["Preferences"],
1123
1427
  summary: "Set an authenticated user preference",
1124
- parameters: [{ name: "key", in: "path", required: true, schema: { type: "string" } }],
1428
+ parameters: [
1429
+ { name: "key", in: "path", required: true, schema: { type: "string" } }
1430
+ ],
1125
1431
  requestBody: {
1126
1432
  required: true,
1127
1433
  content: { "application/json": { schema: { type: "object" } } }
@@ -1157,10 +1463,23 @@ function generateOpenApi(config) {
1157
1463
  tags: ["Audit"],
1158
1464
  summary: "Get audit entries across all readable audited collections",
1159
1465
  parameters: [
1160
- { name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 100 } },
1466
+ {
1467
+ name: "limit",
1468
+ in: "query",
1469
+ schema: { type: "integer", default: 50, maximum: 100 }
1470
+ },
1161
1471
  { name: "page", in: "query", schema: { type: "integer", default: 1 } },
1162
- { name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
1163
- { name: "sort", in: "query", schema: { type: "string", default: "-timestamp" } }
1472
+ {
1473
+ name: "where",
1474
+ in: "query",
1475
+ schema: { type: "string" },
1476
+ description: "JSON filter"
1477
+ },
1478
+ {
1479
+ name: "sort",
1480
+ in: "query",
1481
+ schema: { type: "string", default: "-timestamp" }
1482
+ }
1164
1483
  ],
1165
1484
  responses: {
1166
1485
  200: {
@@ -1365,10 +1684,27 @@ function generateOpenApi(config) {
1365
1684
  tags: [collectionTag],
1366
1685
  summary: `Get ${labels.singular} audit entries`,
1367
1686
  parameters: [
1368
- { name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 100 } },
1369
- { name: "page", in: "query", schema: { type: "integer", default: 1 } },
1370
- { name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
1371
- { name: "sort", in: "query", schema: { type: "string", default: "-timestamp" } }
1687
+ {
1688
+ name: "limit",
1689
+ in: "query",
1690
+ schema: { type: "integer", default: 50, maximum: 100 }
1691
+ },
1692
+ {
1693
+ name: "page",
1694
+ in: "query",
1695
+ schema: { type: "integer", default: 1 }
1696
+ },
1697
+ {
1698
+ name: "where",
1699
+ in: "query",
1700
+ schema: { type: "string" },
1701
+ description: "JSON filter"
1702
+ },
1703
+ {
1704
+ name: "sort",
1705
+ in: "query",
1706
+ schema: { type: "string", default: "-timestamp" }
1707
+ }
1372
1708
  ],
1373
1709
  responses: {
1374
1710
  200: {
@@ -1494,6 +1830,15 @@ function generateOpenApi(config) {
1494
1830
  post: {
1495
1831
  tags: [collectionTag],
1496
1832
  summary: `Log out of ${labels.plural}`,
1833
+ parameters: [
1834
+ {
1835
+ name: "allSessions",
1836
+ in: "query",
1837
+ required: false,
1838
+ schema: { type: "boolean" },
1839
+ description: "Revoke every active session for this account instead of only the current one."
1840
+ }
1841
+ ],
1497
1842
  responses: { 200: { description: "Logged out" } }
1498
1843
  }
1499
1844
  };
@@ -1519,7 +1864,11 @@ function generateOpenApi(config) {
1519
1864
  post: {
1520
1865
  tags: [collectionTag],
1521
1866
  summary: "Refresh an authentication token",
1522
- responses: { 200: { description: "Refreshed token" } }
1867
+ responses: {
1868
+ 200: {
1869
+ description: "Refreshed token for the current active session"
1870
+ }
1871
+ }
1523
1872
  }
1524
1873
  };
1525
1874
  spec.paths[`${path}/forgot-password`] = {
@@ -1550,7 +1899,11 @@ function generateOpenApi(config) {
1550
1899
  schema: { type: "string" }
1551
1900
  }
1552
1901
  ],
1553
- responses: { 200: { description: "Password changed" } }
1902
+ responses: {
1903
+ 200: {
1904
+ description: "Password changed and active sessions revoked"
1905
+ }
1906
+ }
1554
1907
  }
1555
1908
  };
1556
1909
  }
@@ -1759,7 +2112,7 @@ function fieldsToProperties(fields) {
1759
2112
  return { properties: props, required };
1760
2113
  }
1761
2114
  function fieldToSchema(field) {
1762
- let schema = {};
2115
+ let schema;
1763
2116
  switch (field.type) {
1764
2117
  case "text":
1765
2118
  case "textarea":
@@ -1768,7 +2121,10 @@ function fieldToSchema(field) {
1768
2121
  break;
1769
2122
  case "url":
1770
2123
  schema = {
1771
- oneOf: [{ type: "string" }, { type: "object", additionalProperties: true }]
2124
+ oneOf: [
2125
+ { type: "string" },
2126
+ { type: "object", additionalProperties: true }
2127
+ ]
1772
2128
  };
1773
2129
  break;
1774
2130
  case "icon":
@@ -1916,6 +2272,7 @@ function defineTab(args) {
1916
2272
  LIFECYCLE_EVENT_NAMES,
1917
2273
  WORKFLOW_HISTORY_COLLECTION,
1918
2274
  availableWorkflowTransitions,
2275
+ buildSqlOrderBy,
1919
2276
  canViewWorkflowDraft,
1920
2277
  createLifecycleEvent,
1921
2278
  createWorkflowDocument,
@@ -1937,6 +2294,7 @@ function defineTab(args) {
1937
2294
  defineMultiSelectField,
1938
2295
  defineNumberField,
1939
2296
  defineObjectField,
2297
+ definePublishingWorkflow,
1940
2298
  defineRadioField,
1941
2299
  defineRelationshipField,
1942
2300
  defineRichTextField,
@@ -1957,6 +2315,7 @@ function defineTab(args) {
1957
2315
  initializeWorkflowDocument,
1958
2316
  materializeWorkflowDocument,
1959
2317
  normalizeConfig,
2318
+ numericFieldNames,
1960
2319
  parseMongoWhere,
1961
2320
  parseSort,
1962
2321
  parseSqlWhere,
@@ -1964,6 +2323,7 @@ function defineTab(args) {
1964
2323
  resolveAdminAuthCollection,
1965
2324
  runCollectionHooks,
1966
2325
  saveWorkflowDraft,
2326
+ simplePublishingWorkflow,
1967
2327
  transitionWorkflow,
1968
2328
  workflowCapabilities
1969
2329
  });