@dyrected/core 2.9.0 → 2.10.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/index.js CHANGED
@@ -101,7 +101,7 @@ import {
101
101
  validatePreviewUrlExpression,
102
102
  wordCount,
103
103
  workflowCapabilities
104
- } from "./chunk-XRIONGRY.js";
104
+ } from "./chunk-H7DVVW2P.js";
105
105
  import "./chunk-JSBRDJBE.js";
106
106
 
107
107
  // src/types/workflows.ts
@@ -114,44 +114,17 @@ var LIFECYCLE_EVENT_NAMES = [
114
114
 
115
115
  // src/types/views.ts
116
116
  function defineView(config) {
117
- return {
118
- slug: config.slug,
119
- label: config.label,
120
- icon: config.icon,
121
- layout: config.layout ?? "table",
122
- filter: config.filter,
123
- groupBy: config.groupBy,
124
- dateField: config.dateField,
125
- startDateField: config.startDateField,
126
- endDateField: config.endDateField,
127
- columns: config.columns,
128
- sort: config.sort,
129
- actions: config.actions,
130
- features: config.features,
131
- actionOrder: config.actionOrder,
132
- metrics: config.metrics,
133
- access: config.access
134
- };
117
+ return config;
135
118
  }
136
119
  function defineAction(config) {
137
- return {
138
- name: config.name,
139
- label: config.label,
140
- icon: config.icon,
141
- type: config.type ?? "row",
142
- confirm: config.confirm,
143
- fields: config.fields,
144
- mutation: config.mutation,
145
- handler: config.handler,
146
- access: config.access
147
- };
120
+ return config;
148
121
  }
149
122
 
150
123
  // src/utils/parse-where.ts
151
124
  function assertNever(op, context) {
152
125
  throw new Error(`[dyrected/core] Unhandled where operator "${op}" in ${context}`);
153
126
  }
154
- function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "LIKE") {
127
+ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = placeholder === "pg" ? "ILIKE" : "LIKE") {
155
128
  const params = [];
156
129
  let pgIndex = 1;
157
130
  function next() {
@@ -162,8 +135,16 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "L
162
135
  }
163
136
  function buildOperator(field, value) {
164
137
  const c = col(field);
138
+ const isJsonExtract = c.startsWith("data->>") || c.startsWith("json_extract(") || c.startsWith("JSON_UNQUOTE(");
165
139
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
166
- params.push(typeof value === "boolean" ? String(value) : value);
140
+ if (value === null) {
141
+ return `${c} IS NULL`;
142
+ }
143
+ if (value === "" && placeholder === "pg") {
144
+ params.push("");
145
+ return `(${c})::text = ${next()}`;
146
+ }
147
+ params.push(typeof value === "boolean" && isJsonExtract ? String(value) : value);
167
148
  return `${c} = ${next()}`;
168
149
  }
169
150
  const entries = Object.entries(value);
@@ -174,18 +155,63 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "L
174
155
  return buildSingleOp(c, op, operand);
175
156
  }
176
157
  function buildSingleOp(c, op, operand) {
158
+ const isJsonExtract = c.startsWith("data->>") || c.startsWith("json_extract(") || c.startsWith("JSON_UNQUOTE(");
177
159
  switch (op) {
178
160
  case "equals":
179
- params.push(typeof operand === "boolean" ? String(operand) : operand);
161
+ if (operand === null) {
162
+ return `${c} IS NULL`;
163
+ }
164
+ if (operand === "" && placeholder === "pg") {
165
+ params.push("");
166
+ return `(${c})::text = ${next()}`;
167
+ }
168
+ if (typeof operand === "boolean") {
169
+ if (placeholder === "?") {
170
+ params.push(operand ? 1 : 0);
171
+ } else if (isJsonExtract) {
172
+ params.push(String(operand));
173
+ } else {
174
+ params.push(operand);
175
+ }
176
+ } else {
177
+ params.push(operand);
178
+ }
180
179
  return `${c} = ${next()}`;
181
180
  case "not_equals":
182
- params.push(typeof operand === "boolean" ? String(operand) : operand);
181
+ if (operand === null) {
182
+ return `${c} IS NOT NULL`;
183
+ }
184
+ if (operand === "" && placeholder === "pg") {
185
+ params.push("");
186
+ return `(${c})::text != ${next()}`;
187
+ }
188
+ if (typeof operand === "boolean") {
189
+ if (placeholder === "?") {
190
+ params.push(operand ? 1 : 0);
191
+ } else if (isJsonExtract) {
192
+ params.push(String(operand));
193
+ } else {
194
+ params.push(operand);
195
+ }
196
+ } else {
197
+ params.push(operand);
198
+ }
183
199
  return `${c} != ${next()}`;
184
200
  case "in": {
185
201
  const vals = Array.isArray(operand) ? operand : [operand];
186
202
  if (vals.length === 0) return "1=0";
187
203
  const placeholders = vals.map((v) => {
188
- params.push(typeof v === "boolean" ? String(v) : v);
204
+ if (typeof v === "boolean") {
205
+ if (placeholder === "?") {
206
+ params.push(v ? 1 : 0);
207
+ } else if (isJsonExtract) {
208
+ params.push(String(v));
209
+ } else {
210
+ params.push(v);
211
+ }
212
+ } else {
213
+ params.push(v);
214
+ }
189
215
  return next();
190
216
  });
191
217
  return `${c} IN (${placeholders.join(", ")})`;
@@ -194,7 +220,7 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "L
194
220
  const vals = Array.isArray(operand) ? operand : [operand];
195
221
  if (vals.length === 0) return "1=1";
196
222
  const placeholders = vals.map((v) => {
197
- params.push(typeof v === "boolean" ? String(v) : v);
223
+ params.push(typeof v === "boolean" && isJsonExtract ? String(v) : v);
198
224
  return next();
199
225
  });
200
226
  return `${c} NOT IN (${placeholders.join(", ")})`;
@@ -220,10 +246,10 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "L
220
246
  case "contains":
221
247
  case "like":
222
248
  params.push(`%${operand}%`);
223
- return `${c} ${likeOperator} ${next()}`;
249
+ return placeholder === "pg" ? `(${c})::text ${likeOperator} ${next()}` : `${c} ${likeOperator} ${next()}`;
224
250
  case "starts_with":
225
251
  params.push(`${operand}%`);
226
- return `${c} ${likeOperator} ${next()}`;
252
+ return placeholder === "pg" ? `(${c})::text ${likeOperator} ${next()}` : `${c} ${likeOperator} ${next()}`;
227
253
  case "exists":
228
254
  return operand ? `${c} IS NOT NULL` : `${c} IS NULL`;
229
255
  default:
@@ -363,6 +389,346 @@ function buildSqlOrderBy(sort, existingColumns, numericFields, dialect) {
363
389
  }).join(", ");
364
390
  }
365
391
 
392
+ // src/utils/conditions.ts
393
+ function formatJexlValue(v) {
394
+ if (v === null || v === void 0) return "null";
395
+ if (typeof v === "boolean" || typeof v === "number") return String(v);
396
+ if (typeof v === "string") {
397
+ if (v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"') || v.includes("+") || v.includes("?") || v.includes("(") || v.includes(" ")) {
398
+ return v;
399
+ }
400
+ if (v.startsWith("/")) {
401
+ return `'${v}'`;
402
+ }
403
+ if (v === "null" || v === "true" || v === "false") {
404
+ return v;
405
+ }
406
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(v)) {
407
+ return v;
408
+ }
409
+ return `'${v}'`;
410
+ }
411
+ return String(v);
412
+ }
413
+ var MatchBuilder = class {
414
+ branches = [];
415
+ /**
416
+ * Adds a conditional branch (`condition ? value : ...`).
417
+ */
418
+ case(condition, value) {
419
+ this.branches.push({ condition, value });
420
+ return this;
421
+ }
422
+ /**
423
+ * Final fallback value when no cases match.
424
+ */
425
+ otherwise(fallback = "null") {
426
+ if (this.branches.length === 0) return formatJexlValue(fallback);
427
+ let expr = formatJexlValue(fallback);
428
+ for (let i = this.branches.length - 1; i >= 0; i--) {
429
+ const b = this.branches[i];
430
+ expr = `${b.condition} ? ${formatJexlValue(b.value)} : ${expr}`;
431
+ }
432
+ return expr;
433
+ }
434
+ };
435
+ var FieldConditionBuilder = class {
436
+ constructor(field) {
437
+ this.field = field;
438
+ }
439
+ field;
440
+ /**
441
+ * Generates equality condition (`field == value`).
442
+ */
443
+ equals(value) {
444
+ return `${this.field} == ${JSON.stringify(value)}`;
445
+ }
446
+ /**
447
+ * Generates inequality condition (`field != value`).
448
+ */
449
+ notEquals(value) {
450
+ return `${this.field} != ${JSON.stringify(value)}`;
451
+ }
452
+ /**
453
+ * Generates set membership condition (`field in [val1, val2]`).
454
+ */
455
+ in(...values) {
456
+ const flat = values.flat();
457
+ return `${this.field} in [${flat.map((v) => JSON.stringify(v)).join(", ")}]`;
458
+ }
459
+ /**
460
+ * Generates inverted set membership condition (`!(field in [val1, val2])`).
461
+ */
462
+ notIn(...values) {
463
+ const flat = values.flat();
464
+ return `!(${this.field} in [${flat.map((v) => JSON.stringify(v)).join(", ")}])`;
465
+ }
466
+ /**
467
+ * Generates boolean true check (`field == true`).
468
+ */
469
+ isTrue() {
470
+ return `${this.field} == true`;
471
+ }
472
+ /**
473
+ * Generates boolean false check (`field != true`).
474
+ */
475
+ isFalse() {
476
+ return `${this.field} != true`;
477
+ }
478
+ /**
479
+ * Checks that the field is neither null/undefined nor an empty string.
480
+ */
481
+ notEmpty() {
482
+ return `${this.field} != null && ${this.field} != ''`;
483
+ }
484
+ /**
485
+ * Checks that the field is null, undefined, or an empty string.
486
+ */
487
+ isEmpty() {
488
+ return `${this.field} == null || ${this.field} == ''`;
489
+ }
490
+ /**
491
+ * Numerical greater than (`field > num`).
492
+ */
493
+ greaterThan(num) {
494
+ return `${this.field} > ${typeof num === "number" ? num : num}`;
495
+ }
496
+ /**
497
+ * Numerical greater than or equal (`field >= num`).
498
+ */
499
+ greaterThanOrEqual(num) {
500
+ return `${this.field} >= ${typeof num === "number" ? num : num}`;
501
+ }
502
+ /**
503
+ * Numerical less than (`field < num`).
504
+ */
505
+ lessThan(num) {
506
+ return `${this.field} < ${typeof num === "number" ? num : num}`;
507
+ }
508
+ /**
509
+ * Numerical less than or equal (`field <= num`).
510
+ */
511
+ lessThanOrEqual(num) {
512
+ return `${this.field} <= ${typeof num === "number" ? num : num}`;
513
+ }
514
+ /**
515
+ * Numerical range check (`field >= min && field <= max`).
516
+ */
517
+ between(min2, max2) {
518
+ return `${this.field} >= ${min2} && ${this.field} <= ${max2}`;
519
+ }
520
+ /**
521
+ * String prefix check using JEXL `startsWith(field, prefix)`.
522
+ */
523
+ startsWith(prefix) {
524
+ return `startsWith(${this.field}, ${JSON.stringify(prefix)})`;
525
+ }
526
+ /**
527
+ * String suffix check using JEXL `endsWith(field, suffix)`.
528
+ */
529
+ endsWith(suffix) {
530
+ return `endsWith(${this.field}, ${JSON.stringify(suffix)})`;
531
+ }
532
+ /**
533
+ * Substring or array item check using JEXL `includes(field, item)`.
534
+ */
535
+ contains(val) {
536
+ return `includes(${this.field}, ${JSON.stringify(val)})`;
537
+ }
538
+ /**
539
+ * Checks if array/string length exceeds a threshold.
540
+ */
541
+ hasLengthGreaterThan(length2) {
542
+ return `length(${this.field}) > ${length2}`;
543
+ }
544
+ /**
545
+ * Checks if array/string length is at least a threshold.
546
+ */
547
+ hasLengthAtLeast(length2) {
548
+ return `length(${this.field}) >= ${length2}`;
549
+ }
550
+ };
551
+ function createWhen() {
552
+ const whenFn = ((fieldName) => new FieldConditionBuilder(fieldName));
553
+ whenFn.user = (userProp) => new FieldConditionBuilder(`user.${userProp}`);
554
+ whenFn.sibling = (siblingProp) => new FieldConditionBuilder(`siblingData.${siblingProp}`);
555
+ whenFn.concat = (...parts) => {
556
+ if (parts.length === 0) return "''";
557
+ return parts.map((p) => formatJexlValue(p)).join(" + ");
558
+ };
559
+ whenFn.slugify = (field) => `slugify(${field})`;
560
+ whenFn.lower = (field) => `lower(${field})`;
561
+ whenFn.upper = (field) => `upper(${field})`;
562
+ whenFn.trim = (field) => `trim(${field})`;
563
+ whenFn.access = {
564
+ isOwner: (ownerField = "author") => {
565
+ return `${ownerField} == user.id || ${ownerField}.id == user.id`;
566
+ },
567
+ isAdmin: () => {
568
+ return "user.role == 'admin' || (user.roles != null && 'admin' in user.roles)";
569
+ },
570
+ hasRole: (...roles) => {
571
+ return whenFn.userRole(...roles);
572
+ },
573
+ isPublishedOrAdmin: (statusField = "status") => {
574
+ return `${statusField} == 'published' || user.role == 'admin' || (user.roles != null && 'admin' in user.roles)`;
575
+ }
576
+ };
577
+ whenFn.then = (condition, ifTrue, ifFalse = "null") => {
578
+ return `${condition} ? ${formatJexlValue(ifTrue)} : ${formatJexlValue(ifFalse)}`;
579
+ };
580
+ whenFn.match = () => new MatchBuilder();
581
+ whenFn.cases = (...branches) => {
582
+ const builder = new MatchBuilder();
583
+ let fallback = "null";
584
+ for (const b of branches) {
585
+ if (Array.isArray(b)) {
586
+ builder.case(b[0], b[1]);
587
+ } else {
588
+ fallback = b;
589
+ }
590
+ }
591
+ return builder.otherwise(fallback);
592
+ };
593
+ whenFn.all = (...conditions) => {
594
+ const valid = conditions.filter(Boolean);
595
+ if (valid.length === 0) return "true";
596
+ if (valid.length === 1) return valid[0];
597
+ return valid.map((c) => c.includes(" ") ? `(${c})` : c).join(" && ");
598
+ };
599
+ whenFn.and = whenFn.all;
600
+ whenFn.any = (...conditions) => {
601
+ const valid = conditions.filter(Boolean);
602
+ if (valid.length === 0) return "true";
603
+ if (valid.length === 1) return valid[0];
604
+ return valid.map((c) => c.includes(" ") ? `(${c})` : c).join(" || ");
605
+ };
606
+ whenFn.or = whenFn.any;
607
+ whenFn.not = (condition) => {
608
+ if (!condition) return "false";
609
+ return `!(${condition})`;
610
+ };
611
+ whenFn.variant = (...variants) => {
612
+ const flat = variants.flat();
613
+ if (flat.length === 0) return "true";
614
+ if (flat.length === 1) return `variant == '${flat[0]}'`;
615
+ return `variant in [${flat.map((v) => `'${v}'`).join(", ")}]`;
616
+ };
617
+ whenFn.notVariant = (...variants) => {
618
+ const flat = variants.flat();
619
+ if (flat.length === 0) return "false";
620
+ if (flat.length === 1) return `variant != '${flat[0]}'`;
621
+ return `!(variant in [${flat.map((v) => `'${v}'`).join(", ")}])`;
622
+ };
623
+ whenFn.block = (...blocks) => {
624
+ const flat = blocks.flat();
625
+ if (flat.length === 0) return "true";
626
+ if (flat.length === 1) return `block == '${flat[0]}'`;
627
+ return `block in [${flat.map((b) => `'${b}'`).join(", ")}]`;
628
+ };
629
+ whenFn.notBlock = (...blocks) => {
630
+ const flat = blocks.flat();
631
+ if (flat.length === 0) return "false";
632
+ if (flat.length === 1) return `block != '${flat[0]}'`;
633
+ return `!(block in [${flat.map((b) => `'${b}'`).join(", ")}])`;
634
+ };
635
+ whenFn.fieldEquals = (field, value) => {
636
+ return `${field} == ${JSON.stringify(value)}`;
637
+ };
638
+ whenFn.fieldNotEquals = (field, value) => {
639
+ return `${field} != ${JSON.stringify(value)}`;
640
+ };
641
+ whenFn.fieldIn = (field, ...values) => {
642
+ const flat = values.flat();
643
+ return `${field} in [${flat.map((v) => JSON.stringify(v)).join(", ")}]`;
644
+ };
645
+ whenFn.fieldNotIn = (field, ...values) => {
646
+ const flat = values.flat();
647
+ return `!(${field} in [${flat.map((v) => JSON.stringify(v)).join(", ")}])`;
648
+ };
649
+ whenFn.fieldNotEmpty = (field) => {
650
+ return `${field} != null && ${field} != ''`;
651
+ };
652
+ whenFn.fieldEmpty = (field) => {
653
+ return `${field} == null || ${field} == ''`;
654
+ };
655
+ whenFn.fieldIsTrue = (field) => {
656
+ return `${field} == true`;
657
+ };
658
+ whenFn.fieldIsFalse = (field) => {
659
+ return `${field} != true`;
660
+ };
661
+ whenFn.fieldGreaterThan = (field, value) => {
662
+ return `${field} > ${value}`;
663
+ };
664
+ whenFn.fieldGreaterThanOrEqual = (field, value) => {
665
+ return `${field} >= ${value}`;
666
+ };
667
+ whenFn.fieldLessThan = (field, value) => {
668
+ return `${field} < ${value}`;
669
+ };
670
+ whenFn.fieldLessThanOrEqual = (field, value) => {
671
+ return `${field} <= ${value}`;
672
+ };
673
+ whenFn.fieldBetween = (field, min2, max2) => {
674
+ return `${field} >= ${min2} && ${field} <= ${max2}`;
675
+ };
676
+ whenFn.fieldStartsWith = (field, prefix) => {
677
+ return `startsWith(${field}, ${JSON.stringify(prefix)})`;
678
+ };
679
+ whenFn.fieldEndsWith = (field, suffix) => {
680
+ return `endsWith(${field}, ${JSON.stringify(suffix)})`;
681
+ };
682
+ whenFn.fieldContains = (field, item) => {
683
+ return `includes(${field}, ${JSON.stringify(item)})`;
684
+ };
685
+ whenFn.arrayNotEmpty = (field) => {
686
+ return `length(${field}) > 0`;
687
+ };
688
+ whenFn.arrayEmpty = (field) => {
689
+ return `length(${field}) == 0`;
690
+ };
691
+ whenFn.arrayCountGreaterThan = (field, count2) => {
692
+ return `length(${field}) > ${count2}`;
693
+ };
694
+ whenFn.arrayCountAtLeast = (field, count2) => {
695
+ return `length(${field}) >= ${count2}`;
696
+ };
697
+ whenFn.arrayCountLessThan = (field, count2) => {
698
+ return `length(${field}) < ${count2}`;
699
+ };
700
+ whenFn.userRole = (...roles) => {
701
+ const flat = roles.flat();
702
+ if (flat.length === 1) {
703
+ return `user.role == '${flat[0]}' || (user.roles != null && '${flat[0]}' in user.roles)`;
704
+ }
705
+ const roleArray = `[${flat.map((r) => `'${r}'`).join(", ")}]`;
706
+ return `user.role in ${roleArray} || (user.roles != null && includes(${roleArray}, user.role))`;
707
+ };
708
+ whenFn.userEmailDomain = (domain) => {
709
+ const clean = domain.startsWith("@") ? domain : `@${domain}`;
710
+ return `endsWith(user.email, '${clean}')`;
711
+ };
712
+ whenFn.userAttributeEquals = (attr, value) => {
713
+ return `user.${attr} == ${JSON.stringify(value)}`;
714
+ };
715
+ whenFn.isNewDocument = () => {
716
+ return "id == null";
717
+ };
718
+ whenFn.isExistingDocument = () => {
719
+ return "id != null";
720
+ };
721
+ whenFn.statusEquals = (status) => {
722
+ return `status == '${status}'`;
723
+ };
724
+ whenFn.statusIn = (...statuses) => {
725
+ const flat = statuses.flat();
726
+ return `status in [${flat.map((s) => `'${s}'`).join(", ")}]`;
727
+ };
728
+ return whenFn;
729
+ }
730
+ var when = createWhen();
731
+
366
732
  // src/index.ts
367
733
  function defineCollection(config) {
368
734
  return config;
@@ -413,8 +779,10 @@ function defineTab(args) {
413
779
  export {
414
780
  BUILTIN_JEXL_HELPERS,
415
781
  ConfigValidationError,
782
+ FieldConditionBuilder,
416
783
  LIFECYCLE_EVENTS_COLLECTION,
417
784
  LIFECYCLE_EVENT_NAMES,
785
+ MatchBuilder,
418
786
  WORKFLOW_HISTORY_COLLECTION,
419
787
  abs,
420
788
  addDays,
@@ -496,6 +864,7 @@ export {
496
864
  floor,
497
865
  formatConfigDiagnostics,
498
866
  formatDate,
867
+ formatJexlValue,
499
868
  generateDefaultDetailSchema,
500
869
  generateOpenApi,
501
870
  get,
@@ -547,6 +916,7 @@ export {
547
916
  validateDeclarativeAccessExpression,
548
917
  validateDeclarativeHookExpression,
549
918
  validatePreviewUrlExpression,
919
+ when,
550
920
  wordCount,
551
921
  workflowCapabilities
552
922
  };