@stndrds/schema 0.1.0-alpha.57 → 0.1.0-alpha.59

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.
@@ -1,40 +1,22 @@
1
+ import {
2
+ asTenantId,
3
+ asUserId,
4
+ generateId
5
+ } from "./chunk-V2RPPE2Y.mjs";
6
+ import {
7
+ computeRecordStatus,
8
+ formatZodErrors,
9
+ parseAttributeConfig,
10
+ validateDraftOrThrow,
11
+ validateObject,
12
+ validateObjectOrThrow
13
+ } from "./chunk-SV4BCGQU.mjs";
1
14
  import {
2
15
  __require
3
16
  } from "./chunk-Y6FXYEAI.mjs";
4
17
 
5
18
  // src/runtime/auth/workflow-jwt.service.ts
6
19
  import { SignJWT, importPKCS8, importSPKI, jwtVerify } from "jose";
7
-
8
- // src/utils.ts
9
- function asTenantId(id) {
10
- return id;
11
- }
12
- function asUserId(id) {
13
- return id;
14
- }
15
- function generateId() {
16
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
17
- return crypto.randomUUID();
18
- }
19
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
20
- const r = Math.random() * 16 | 0;
21
- const v = c === "x" ? r : r & 3 | 8;
22
- return v.toString(16);
23
- });
24
- }
25
- function generatePrefixedId(prefix) {
26
- return `${prefix}_${generateId()}`;
27
- }
28
- function slugify(input) {
29
- return input.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9\s_-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-");
30
- }
31
- function generateTemplateName(label) {
32
- const slug = slugify(label) || "template";
33
- const suffix = generateId().slice(0, 8);
34
- return `${slug}-${suffix}`;
35
- }
36
-
37
- // src/runtime/auth/workflow-jwt.service.ts
38
20
  var WorkflowJwtService = class _WorkflowJwtService {
39
21
  constructor(config) {
40
22
  this.config = config;
@@ -212,6 +194,26 @@ var WorkflowJwtService = class _WorkflowJwtService {
212
194
  }
213
195
  };
214
196
 
197
+ // src/lib/object-helpers.ts
198
+ function isEmpty(obj) {
199
+ if (obj === null || obj === void 0) {
200
+ return true;
201
+ }
202
+ if (typeof obj !== "object") {
203
+ return false;
204
+ }
205
+ return Object.keys(obj).length === 0;
206
+ }
207
+ function isNotEmpty(obj) {
208
+ return obj !== null && obj !== void 0 && typeof obj === "object" && Object.keys(obj).length > 0;
209
+ }
210
+ function toUndefinedIfEmpty(obj) {
211
+ return isNotEmpty(obj) ? obj : void 0;
212
+ }
213
+ function hasProperties(obj) {
214
+ return isNotEmpty(obj);
215
+ }
216
+
215
217
  // src/runtime/cache.ts
216
218
  function fnv1aHash(str) {
217
219
  let hash = 2166136261;
@@ -222,7 +224,7 @@ function fnv1aHash(str) {
222
224
  return hash.toString(16).padStart(8, "0");
223
225
  }
224
226
  function hashOptions(options) {
225
- if (options === null || options === void 0 || typeof options === "object" && Object.keys(options).length === 0) {
227
+ if (isEmpty(options)) {
226
228
  return "default";
227
229
  }
228
230
  const sortedJson = JSON.stringify(options, (_, value) => {
@@ -1345,12 +1347,6 @@ function or(...rules) {
1345
1347
  function inValues(field, values) {
1346
1348
  return { field, operator: "in", value: values };
1347
1349
  }
1348
- function isEmpty(field) {
1349
- return { field, operator: "isEmpty", value: null };
1350
- }
1351
- function isNotEmpty(field) {
1352
- return { field, operator: "isNotEmpty", value: null };
1353
- }
1354
1350
 
1355
1351
  // src/types/workflows/definition.ts
1356
1352
  function isWorkflowDefinition(obj) {
@@ -1460,16 +1456,6 @@ function setContextValue(context, path, value) {
1460
1456
  }
1461
1457
  current[parts[parts.length - 1]] = value;
1462
1458
  }
1463
- function mergeFormToSlot(context, nodeId, slotId) {
1464
- const formData = context.forms[nodeId];
1465
- if (!formData) {
1466
- return;
1467
- }
1468
- if (!context.slots[slotId]) {
1469
- context.slots[slotId] = {};
1470
- }
1471
- Object.assign(context.slots[slotId], formData);
1472
- }
1473
1459
 
1474
1460
  // src/types/workflows/theme.ts
1475
1461
  var DEFAULT_THEME = {
@@ -1823,9 +1809,9 @@ function compareValues(actual, operator, expected) {
1823
1809
  return typeof actual === "string" && typeof expected === "string" ? actual.endsWith(expected) : false;
1824
1810
  // Presence
1825
1811
  case "isEmpty":
1826
- return isEmpty2(actual);
1812
+ return isEmptyValue(actual);
1827
1813
  case "isNotEmpty":
1828
- return !isEmpty2(actual);
1814
+ return !isEmptyValue(actual);
1829
1815
  // Set membership
1830
1816
  case "in":
1831
1817
  if (Array.isArray(expected)) {
@@ -1841,7 +1827,7 @@ function compareValues(actual, operator, expected) {
1841
1827
  return false;
1842
1828
  }
1843
1829
  }
1844
- function isEmpty2(value) {
1830
+ function isEmptyValue(value) {
1845
1831
  if (value === null || value === void 0) {
1846
1832
  return true;
1847
1833
  }
@@ -1851,7 +1837,7 @@ function isEmpty2(value) {
1851
1837
  if (Array.isArray(value) && value.length === 0) {
1852
1838
  return true;
1853
1839
  }
1854
- if (typeof value === "object" && Object.keys(value).length === 0) {
1840
+ if (typeof value === "object" && isEmpty(value)) {
1855
1841
  return true;
1856
1842
  }
1857
1843
  return false;
@@ -2096,7 +2082,7 @@ var DocumentExecutor = class {
2096
2082
  validateSlotReferences(node, workflowSlots) {
2097
2083
  const errors = [];
2098
2084
  if (node.targetSlotIds && node.targetSlotIds.length > 0) {
2099
- const slotIdSet = new Set(workflowSlots.map((s) => s.id));
2085
+ const slotIdSet = workflowSlots.reduce((set, s) => set.add(s.id), /* @__PURE__ */ new Set());
2100
2086
  for (const slotId of node.targetSlotIds) {
2101
2087
  if (!slotIdSet.has(slotId)) {
2102
2088
  errors.push(
@@ -2132,24 +2118,25 @@ var FormExecutor = class {
2132
2118
  }
2133
2119
  execute(node, context) {
2134
2120
  const { input } = context;
2135
- if (!input || Object.keys(input).length === 0) {
2121
+ if (isEmpty(input)) {
2136
2122
  const requiredParticipationId = node.participantId ?? void 0;
2137
2123
  return wait(`Waiting for form submission: ${node.label}`, {
2138
2124
  requiredParticipationId
2139
2125
  });
2140
2126
  }
2127
+ const formInput = input;
2141
2128
  const slotIds = this.extractSlotIds(node);
2142
2129
  if (slotIds.size === 0) {
2143
2130
  return error("MISSING_FIELDS", "FormNode must have fields or rows with slot references");
2144
2131
  }
2145
2132
  const contextUpdates = {
2146
2133
  forms: {
2147
- [node.id]: input
2134
+ [node.id]: formInput
2148
2135
  },
2149
2136
  slots: {}
2150
2137
  };
2151
2138
  for (const slotId of slotIds) {
2152
- const slotInput = input[slotId] ?? {};
2139
+ const slotInput = formInput[slotId] ?? {};
2153
2140
  if (!contextUpdates.slots) {
2154
2141
  contextUpdates.slots = {};
2155
2142
  }
@@ -2177,9 +2164,9 @@ var FormExecutor = class {
2177
2164
  }
2178
2165
  canExecute(node, context) {
2179
2166
  if (node.participantId && context.executorId) {
2180
- return context.input !== void 0 && Object.keys(context.input).length > 0;
2167
+ return hasProperties(context.input);
2181
2168
  }
2182
- return context.input !== void 0 && Object.keys(context.input).length > 0;
2169
+ return hasProperties(context.input);
2183
2170
  }
2184
2171
  validate(node) {
2185
2172
  const errors = [];
@@ -3471,9 +3458,6 @@ var ConcurrentModificationError = class extends SchemaError {
3471
3458
  );
3472
3459
  }
3473
3460
  };
3474
- function isConcurrentModificationError(error2) {
3475
- return error2 instanceof ConcurrentModificationError;
3476
- }
3477
3461
 
3478
3462
  // src/format.ts
3479
3463
  import { getCountryByIso3 } from "@stndrds/constants";
@@ -4075,6 +4059,91 @@ function createMockObjectRecordsRepository(stores) {
4075
4059
  };
4076
4060
  }
4077
4061
 
4062
+ // src/runtime/mock/mock-relation-attributes.ts
4063
+ import { randomUUID } from "crypto";
4064
+ function createMockRelationAttributesRepository(stores) {
4065
+ return {
4066
+ async upsertBatch(items) {
4067
+ const context = getContext2();
4068
+ if (!context) {
4069
+ throw new Error("Context required for relationAttributes operations");
4070
+ }
4071
+ const results = [];
4072
+ for (const item of items) {
4073
+ const _key = `${context.tenantId}:${item.fromObject}:${item.fromId}:${item.fromAttribute}:${item.toId}`;
4074
+ const existing = Array.from(stores.relationAttributes.values()).find(
4075
+ (row) => row.tenantId === context.tenantId && row.fromObject === item.fromObject && row.fromId === item.fromId && row.fromAttribute === item.fromAttribute && row.toId === item.toId
4076
+ );
4077
+ if (existing) {
4078
+ existing.properties = item.properties ?? {};
4079
+ existing.updatedAt = /* @__PURE__ */ new Date();
4080
+ existing.updatedBy = item.updatedBy ?? context.userId ?? null;
4081
+ results.push(existing);
4082
+ } else {
4083
+ const row = {
4084
+ id: randomUUID(),
4085
+ tenantId: context.tenantId,
4086
+ fromObject: item.fromObject,
4087
+ fromId: item.fromId,
4088
+ fromAttribute: item.fromAttribute,
4089
+ toId: item.toId,
4090
+ properties: item.properties ?? {},
4091
+ createdAt: /* @__PURE__ */ new Date(),
4092
+ updatedAt: /* @__PURE__ */ new Date(),
4093
+ createdBy: item.createdBy ?? context.userId ?? null,
4094
+ updatedBy: item.updatedBy ?? context.userId ?? null
4095
+ };
4096
+ stores.relationAttributes.set(row.id, row);
4097
+ results.push(row);
4098
+ }
4099
+ }
4100
+ return results;
4101
+ },
4102
+ async findBySource(fromObject, fromId, fromAttribute) {
4103
+ const context = getContext2();
4104
+ if (!context) {
4105
+ throw new Error("Context required for relationAttributes operations");
4106
+ }
4107
+ return Array.from(stores.relationAttributes.values()).filter(
4108
+ (row) => row.tenantId === context.tenantId && row.fromObject === fromObject && row.fromId === fromId && row.fromAttribute === fromAttribute
4109
+ );
4110
+ },
4111
+ async findByTarget(toId) {
4112
+ const context = getContext2();
4113
+ if (!context) {
4114
+ throw new Error("Context required for relationAttributes operations");
4115
+ }
4116
+ return Array.from(stores.relationAttributes.values()).filter(
4117
+ (row) => row.tenantId === context.tenantId && row.toId === toId
4118
+ );
4119
+ },
4120
+ async deleteBySource(fromObject, fromId, fromAttribute) {
4121
+ const context = getContext2();
4122
+ if (!context) {
4123
+ throw new Error("Context required for relationAttributes operations");
4124
+ }
4125
+ const toDelete = Array.from(stores.relationAttributes.entries()).filter(
4126
+ ([_id, row]) => row.tenantId === context.tenantId && row.fromObject === fromObject && row.fromId === fromId && row.fromAttribute === fromAttribute
4127
+ );
4128
+ for (const [id] of toDelete) {
4129
+ stores.relationAttributes.delete(id);
4130
+ }
4131
+ },
4132
+ async deleteByTarget(toId) {
4133
+ const context = getContext2();
4134
+ if (!context) {
4135
+ throw new Error("Context required for relationAttributes operations");
4136
+ }
4137
+ const toDelete = Array.from(stores.relationAttributes.entries()).filter(
4138
+ ([_id, row]) => row.tenantId === context.tenantId && row.toId === toId
4139
+ );
4140
+ for (const [id] of toDelete) {
4141
+ stores.relationAttributes.delete(id);
4142
+ }
4143
+ }
4144
+ };
4145
+ }
4146
+
4078
4147
  // src/runtime/mock/mock-stores.ts
4079
4148
  function createEmptyStores() {
4080
4149
  return {
@@ -4095,7 +4164,8 @@ function createEmptyStores() {
4095
4164
  aiConversations: /* @__PURE__ */ new Map(),
4096
4165
  aiMessages: /* @__PURE__ */ new Map(),
4097
4166
  aiUserMemory: /* @__PURE__ */ new Map(),
4098
- aiUsageMetrics: /* @__PURE__ */ new Map()
4167
+ aiUsageMetrics: /* @__PURE__ */ new Map(),
4168
+ relationAttributes: /* @__PURE__ */ new Map()
4099
4169
  };
4100
4170
  }
4101
4171
 
@@ -4302,7 +4372,8 @@ function createMockPermissionsRepository(stores) {
4302
4372
  getUserRoles(userProfileId) {
4303
4373
  const tenantId = getTenantId();
4304
4374
  const roleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
4305
- const roles = Array.from(stores.roles.values()).filter((r) => roleIds.includes(r.id));
4375
+ const roleIdsSet = new Set(roleIds);
4376
+ const roles = Array.from(stores.roles.values()).filter((r) => roleIdsSet.has(r.id));
4306
4377
  return Promise.resolve(roles);
4307
4378
  },
4308
4379
  assignRole(input) {
@@ -4518,7 +4589,7 @@ function createMockViewOverlaysRepository(stores) {
4518
4589
  const views = Array.from(stores.views.values()).filter(
4519
4590
  (v) => v.tenantId === tenantId && v.objectName === objectName && v.type === type
4520
4591
  );
4521
- const viewIds = new Set(views.map((v) => v.id));
4592
+ const viewIds = views.reduce((set, v) => set.add(v.id), /* @__PURE__ */ new Set());
4522
4593
  return Promise.resolve(
4523
4594
  Array.from(stores.viewOverlays.values()).find(
4524
4595
  (o) => o.tenantId === tenantId && o.userId === userId && o.isUserDefault === true && viewIds.has(o.viewId)
@@ -4603,7 +4674,7 @@ function createMockViewOverlaysRepository(stores) {
4603
4674
  const views = Array.from(stores.views.values()).filter(
4604
4675
  (v) => v.tenantId === tenantId && v.objectName === objectName && v.type === type
4605
4676
  );
4606
- const viewIds = new Set(views.map((v) => v.id));
4677
+ const viewIds = views.reduce((set, v) => set.add(v.id), /* @__PURE__ */ new Set());
4607
4678
  for (const overlay of stores.viewOverlays.values()) {
4608
4679
  if (overlay.tenantId === tenantId && overlay.userId === userId && overlay.isUserDefault === true && viewIds.has(overlay.viewId)) {
4609
4680
  overlay.isUserDefault = false;
@@ -5009,6 +5080,8 @@ function createMockAdapter() {
5009
5080
  aiConversations: createMockAIConversationsRepository(stores),
5010
5081
  aiUserMemory: createMockAIUserMemoryRepository(stores),
5011
5082
  aiUsageMetrics: createMockAIUsageMetricsRepository(stores),
5083
+ // Relation attributes repository
5084
+ relationAttributes: createMockRelationAttributesRepository(stores),
5012
5085
  async transaction(callback) {
5013
5086
  return await callback(adapter);
5014
5087
  },
@@ -5034,6 +5107,7 @@ function createMockAdapter() {
5034
5107
  stores.aiMessages.clear();
5035
5108
  stores.aiUserMemory.clear();
5036
5109
  stores.aiUsageMetrics.clear();
5110
+ stores.relationAttributes.clear();
5037
5111
  }
5038
5112
  };
5039
5113
  return adapter;
@@ -5512,6 +5586,292 @@ function validateOptions(options, attributeName) {
5512
5586
  }
5513
5587
  }
5514
5588
 
5589
+ // src/types/relation-properties.ts
5590
+ var FORBIDDEN_PROPERTY_TYPES = [
5591
+ "formula",
5592
+ "rollup",
5593
+ "relation",
5594
+ "file",
5595
+ "user",
5596
+ "document",
5597
+ "richtext"
5598
+ ];
5599
+
5600
+ // src/builders/property-schema-builder.ts
5601
+ var PropertySchemaBuilder = class {
5602
+ constructor() {
5603
+ this.definitions = [];
5604
+ }
5605
+ /**
5606
+ * Add a property to the schema
5607
+ * The type of property is automatically detected based on the builder methods used
5608
+ */
5609
+ add(name, configure) {
5610
+ const builder = new PropertyTypeBuilder(name);
5611
+ const configured = configure(builder);
5612
+ const definition = configured.build();
5613
+ this.definitions.push(definition);
5614
+ return this;
5615
+ }
5616
+ /**
5617
+ * Build the final PropertySchema
5618
+ */
5619
+ build() {
5620
+ return {
5621
+ definitions: this.definitions
5622
+ };
5623
+ }
5624
+ };
5625
+ var PropertyTypeBuilder = class {
5626
+ constructor(name) {
5627
+ this.name = name;
5628
+ }
5629
+ // Explicit type constructors
5630
+ text() {
5631
+ return new TextPropertyBuilder(this.name);
5632
+ }
5633
+ textarea() {
5634
+ return new TextareaPropertyBuilder(this.name);
5635
+ }
5636
+ number() {
5637
+ return new NumberPropertyBuilder(this.name);
5638
+ }
5639
+ checkbox() {
5640
+ return new CheckboxPropertyBuilder(this.name);
5641
+ }
5642
+ date() {
5643
+ return new DatePropertyBuilder(this.name);
5644
+ }
5645
+ phone() {
5646
+ return new PhonePropertyBuilder(this.name);
5647
+ }
5648
+ currency() {
5649
+ return new CurrencyPropertyBuilder(this.name);
5650
+ }
5651
+ status() {
5652
+ return new StatusPropertyBuilder(this.name);
5653
+ }
5654
+ select() {
5655
+ return new SelectPropertyBuilder(this.name);
5656
+ }
5657
+ multiselect() {
5658
+ return new MultiselectPropertyBuilder(this.name);
5659
+ }
5660
+ rating() {
5661
+ return new RatingPropertyBuilder(this.name);
5662
+ }
5663
+ location() {
5664
+ return new LocationPropertyBuilder(this.name);
5665
+ }
5666
+ };
5667
+ var BasePropertyBuilder = class {
5668
+ constructor(name) {
5669
+ this.definition = { name };
5670
+ }
5671
+ /**
5672
+ * Set the label
5673
+ */
5674
+ label(label) {
5675
+ this.definition.label = label;
5676
+ return this;
5677
+ }
5678
+ /**
5679
+ * Mark as required
5680
+ */
5681
+ required() {
5682
+ this.definition.required = true;
5683
+ return this;
5684
+ }
5685
+ /**
5686
+ * Set description
5687
+ */
5688
+ description(description) {
5689
+ this.definition.description = description;
5690
+ return this;
5691
+ }
5692
+ /**
5693
+ * Build the final definition
5694
+ */
5695
+ build() {
5696
+ return this.definition;
5697
+ }
5698
+ };
5699
+ var TextPropertyBuilder = class extends BasePropertyBuilder {
5700
+ constructor(name) {
5701
+ super(name);
5702
+ this.definition.type = "text";
5703
+ }
5704
+ minLength(value) {
5705
+ this.definition.minLength = value;
5706
+ return this;
5707
+ }
5708
+ maxLength(value) {
5709
+ this.definition.maxLength = value;
5710
+ return this;
5711
+ }
5712
+ pattern(pattern) {
5713
+ this.definition.pattern = pattern;
5714
+ return this;
5715
+ }
5716
+ placeholder(value) {
5717
+ this.definition.placeholder = value;
5718
+ return this;
5719
+ }
5720
+ };
5721
+ var TextareaPropertyBuilder = class extends BasePropertyBuilder {
5722
+ constructor(name) {
5723
+ super(name);
5724
+ this.definition.type = "textarea";
5725
+ }
5726
+ minLength(value) {
5727
+ this.definition.minLength = value;
5728
+ return this;
5729
+ }
5730
+ maxLength(value) {
5731
+ this.definition.maxLength = value;
5732
+ return this;
5733
+ }
5734
+ placeholder(value) {
5735
+ this.definition.placeholder = value;
5736
+ return this;
5737
+ }
5738
+ };
5739
+ var NumberPropertyBuilder = class extends BasePropertyBuilder {
5740
+ constructor(name) {
5741
+ super(name);
5742
+ this.definition.type = "number";
5743
+ }
5744
+ min(value) {
5745
+ this.definition.min = value;
5746
+ return this;
5747
+ }
5748
+ max(value) {
5749
+ this.definition.max = value;
5750
+ return this;
5751
+ }
5752
+ decimal(places) {
5753
+ this.definition.decimal = places;
5754
+ return this;
5755
+ }
5756
+ integer() {
5757
+ this.definition.integer = true;
5758
+ return this;
5759
+ }
5760
+ placeholder(value) {
5761
+ this.definition.placeholder = value;
5762
+ return this;
5763
+ }
5764
+ };
5765
+ var CheckboxPropertyBuilder = class extends BasePropertyBuilder {
5766
+ constructor(name) {
5767
+ super(name);
5768
+ this.definition.type = "checkbox";
5769
+ }
5770
+ };
5771
+ var DatePropertyBuilder = class extends BasePropertyBuilder {
5772
+ constructor(name) {
5773
+ super(name);
5774
+ this.definition.type = "date";
5775
+ }
5776
+ includeTime() {
5777
+ this.definition.includeTime = true;
5778
+ return this;
5779
+ }
5780
+ min(date2) {
5781
+ this.definition.min = date2;
5782
+ return this;
5783
+ }
5784
+ max(date2) {
5785
+ this.definition.max = date2;
5786
+ return this;
5787
+ }
5788
+ };
5789
+ var PhonePropertyBuilder = class extends BasePropertyBuilder {
5790
+ constructor(name) {
5791
+ super(name);
5792
+ this.definition.type = "phone";
5793
+ }
5794
+ };
5795
+ var CurrencyPropertyBuilder = class extends BasePropertyBuilder {
5796
+ constructor(name) {
5797
+ super(name);
5798
+ this.definition.type = "currency";
5799
+ }
5800
+ currency(code) {
5801
+ this.definition.currency = code;
5802
+ return this;
5803
+ }
5804
+ min(value) {
5805
+ this.definition.min = value;
5806
+ return this;
5807
+ }
5808
+ max(value) {
5809
+ this.definition.max = value;
5810
+ return this;
5811
+ }
5812
+ };
5813
+ var StatusPropertyBuilder = class extends BasePropertyBuilder {
5814
+ constructor(name) {
5815
+ super(name);
5816
+ this.definition.type = "status";
5817
+ }
5818
+ options(options) {
5819
+ this.definition.options = options;
5820
+ return this;
5821
+ }
5822
+ };
5823
+ var SelectPropertyBuilder = class extends BasePropertyBuilder {
5824
+ constructor(name) {
5825
+ super(name);
5826
+ this.definition.type = "select";
5827
+ }
5828
+ options(options) {
5829
+ this.definition.options = options;
5830
+ return this;
5831
+ }
5832
+ };
5833
+ var MultiselectPropertyBuilder = class extends BasePropertyBuilder {
5834
+ constructor(name) {
5835
+ super(name);
5836
+ this.definition.type = "multiselect";
5837
+ }
5838
+ options(options) {
5839
+ this.definition.options = options;
5840
+ return this;
5841
+ }
5842
+ maxSelections(value) {
5843
+ this.definition.maxSelections = value;
5844
+ return this;
5845
+ }
5846
+ };
5847
+ var RatingPropertyBuilder = class extends BasePropertyBuilder {
5848
+ constructor(name) {
5849
+ super(name);
5850
+ this.definition.type = "rating";
5851
+ }
5852
+ max(value) {
5853
+ this.definition.max = value;
5854
+ return this;
5855
+ }
5856
+ icon(icon) {
5857
+ this.definition.icon = icon;
5858
+ return this;
5859
+ }
5860
+ };
5861
+ var LocationPropertyBuilder = class extends BasePropertyBuilder {
5862
+ constructor(name) {
5863
+ super(name);
5864
+ this.definition.type = "location";
5865
+ }
5866
+ };
5867
+ function validatePropertyType(type) {
5868
+ if (FORBIDDEN_PROPERTY_TYPES.includes(type)) {
5869
+ throw new Error(
5870
+ `Property type "${type}" is not supported in .qualifyWith(). Only simple types (text, number, date, select, etc.) are allowed. Complex types (formula, rollup, relation, file, user, document) would require duplicating backend behavior.`
5871
+ );
5872
+ }
5873
+ }
5874
+
5515
5875
  // src/builders/attribute-builders.ts
5516
5876
  var BaseAttributeBuilder = class {
5517
5877
  constructor(type, name, label) {
@@ -6068,6 +6428,32 @@ var SingleRelationAttributeBuilder = class extends BaseAttributeBuilder {
6068
6428
  );
6069
6429
  return multiBuilder;
6070
6430
  }
6431
+ /**
6432
+ * Add properties to qualify the relation
6433
+ * Must be called AFTER .to() to ensure targets are defined
6434
+ *
6435
+ * @example
6436
+ * ```typescript
6437
+ * relation({ name: "mainCompany", label: "Main Company" })
6438
+ * .to("companies")
6439
+ * .qualifyWith(props => props
6440
+ * .add("role", select => select.options([...]).required())
6441
+ * .add("shares", number => number.min(0))
6442
+ * )
6443
+ * ```
6444
+ */
6445
+ qualifyWith(configure) {
6446
+ const targets = this.attr.targets;
6447
+ if (!targets || targets.length === 0) {
6448
+ throw new Error(
6449
+ '.qualifyWith() must be called AFTER .to(). Example: relation({ name: "mainCompany" }).to("companies").qualifyWith(...)'
6450
+ );
6451
+ }
6452
+ const builder = new PropertySchemaBuilder();
6453
+ const schema = configure(builder).build();
6454
+ this.attr.properties = schema;
6455
+ return this;
6456
+ }
6071
6457
  required() {
6072
6458
  this.setRequired(true);
6073
6459
  return this;
@@ -6129,6 +6515,33 @@ var MultiRelationAttributeBuilder = class extends BaseAttributeBuilder {
6129
6515
  this.attr.maxItems = count;
6130
6516
  return this;
6131
6517
  }
6518
+ /**
6519
+ * Add properties to qualify the relation
6520
+ * Must be called AFTER .to() or .many() to ensure targets are defined
6521
+ *
6522
+ * @example
6523
+ * ```typescript
6524
+ * relation({ name: "companies", label: "Companies" })
6525
+ * .to("companies")
6526
+ * .many()
6527
+ * .qualifyWith(props => props
6528
+ * .add("role", select => select.options([...]).required())
6529
+ * .add("shares", number => number.min(0))
6530
+ * )
6531
+ * ```
6532
+ */
6533
+ qualifyWith(configure) {
6534
+ const targets = this.attr.targets;
6535
+ if (!targets || targets.length === 0) {
6536
+ throw new Error(
6537
+ '.qualifyWith() must be called AFTER .to() or .many(). Example: relation({ name: "companies" }).to("companies").many().qualifyWith(...)'
6538
+ );
6539
+ }
6540
+ const builder = new PropertySchemaBuilder();
6541
+ const schema = configure(builder).build();
6542
+ this.attr.properties = schema;
6543
+ return this;
6544
+ }
6132
6545
  required() {
6133
6546
  this.setRequired(true);
6134
6547
  return this;
@@ -6514,7 +6927,7 @@ function object(config) {
6514
6927
  }
6515
6928
 
6516
6929
  // src/builders/view-builder.ts
6517
- import { randomUUID } from "crypto";
6930
+ import { randomUUID as randomUUID2 } from "crypto";
6518
6931
  import { z as z3 } from "zod";
6519
6932
  var GroupBuilder = class {
6520
6933
  constructor(id, label) {
@@ -7374,7 +7787,7 @@ var ListViewBuilder = class {
7374
7787
  columns: this.data.columns,
7375
7788
  columnSizing: this.data.columnSizing,
7376
7789
  defaultFilters: this.data.defaultFilters ? {
7377
- id: randomUUID(),
7790
+ id: randomUUID2(),
7378
7791
  combinator: this.data.defaultFilters.combinator,
7379
7792
  rules: this.data.defaultFilters.rules
7380
7793
  } : void 0,
@@ -7385,7 +7798,7 @@ var ListViewBuilder = class {
7385
7798
  label: tab.label,
7386
7799
  icon: tab.icon,
7387
7800
  filters: tab.filters ? {
7388
- id: randomUUID(),
7801
+ id: randomUUID2(),
7389
7802
  combinator: tab.filters.combinator,
7390
7803
  rules: tab.filters.rules
7391
7804
  } : void 0,
@@ -7899,13 +8312,13 @@ var WorkflowBuilder = class {
7899
8312
  if (!this.startNodeId) {
7900
8313
  throw new Error("[WorkflowBuilder] A start node is required. Use .start() to add one.");
7901
8314
  }
7902
- if (!this.data.nodes || Object.keys(this.data.nodes).length === 0) {
8315
+ if (isEmpty(this.data.nodes)) {
7903
8316
  throw new Error("[WorkflowBuilder] At least one node is required.");
7904
8317
  }
7905
8318
  if (!this.data.slots || this.data.slots.length === 0) {
7906
8319
  throw new Error("[WorkflowBuilder] At least one slot is required. Use .slot() to add slots.");
7907
8320
  }
7908
- const hasEndNode = Object.values(this.data.nodes).some((n) => n.type === "end");
8321
+ const hasEndNode = this.data.nodes ? Object.values(this.data.nodes).some((n) => n.type === "end") : false;
7909
8322
  if (!hasEndNode) {
7910
8323
  throw new Error(
7911
8324
  "[WorkflowBuilder] At least one end node is required. Use .end() to add one."
@@ -7936,7 +8349,7 @@ var WorkflowBuilder = class {
7936
8349
  }
7937
8350
  }
7938
8351
  validateSlotReferences() {
7939
- const slotIds = new Set(this.data.slots?.map((s) => s.id) ?? []);
8352
+ const slotIds = this.data.slots?.reduce((set, s) => set.add(s.id), /* @__PURE__ */ new Set()) ?? /* @__PURE__ */ new Set();
7940
8353
  for (const node of Object.values(this.data.nodes ?? {})) {
7941
8354
  if (node.type === "form") {
7942
8355
  const referencedSlots = /* @__PURE__ */ new Set();
@@ -8041,22 +8454,6 @@ function isBehaviorProperty(property) {
8041
8454
  function isPresentationProperty(property) {
8042
8455
  return PRESENTATION_PROPERTIES.includes(property);
8043
8456
  }
8044
- function getPropertyProtectionLevel(property) {
8045
- if (isIdentityProperty(property)) return "identity";
8046
- if (isBehaviorProperty(property)) return "behavior";
8047
- if (isPresentationProperty(property)) return "presentation";
8048
- return "unknown";
8049
- }
8050
- function filterPropertiesByCategory(properties, category) {
8051
- switch (category) {
8052
- case "identity":
8053
- return properties.filter(isIdentityProperty);
8054
- case "behavior":
8055
- return properties.filter(isBehaviorProperty);
8056
- case "presentation":
8057
- return properties.filter(isPresentationProperty);
8058
- }
8059
- }
8060
8457
 
8061
8458
  // src/types/errors.ts
8062
8459
  var RecordReferencedError = class extends Error {
@@ -8075,656 +8472,99 @@ var AttributeInUseError = class extends Error {
8075
8472
  this.attributeName = attributeName;
8076
8473
  this.usage = usage;
8077
8474
  this.code = "ATTRIBUTE_IN_USE";
8078
- this.name = "AttributeInUseError";
8079
- }
8080
- };
8081
- var ObjectReferencedError = class extends Error {
8082
- constructor(objectName, referencingObjects) {
8083
- super(
8084
- `Cannot delete object "${objectName}": target of relations in ${referencingObjects.join(", ")}`
8085
- );
8086
- this.objectName = objectName;
8087
- this.referencingObjects = referencingObjects;
8088
- this.code = "OBJECT_REFERENCED";
8089
- this.name = "ObjectReferencedError";
8090
- }
8091
- };
8092
-
8093
- // src/types/system-attributes.ts
8094
- var SYSTEM_ATTRIBUTES = {
8095
- createdAt: {
8096
- id: "system:createdAt",
8097
- name: "createdAt",
8098
- label: "Created time",
8099
- type: "date",
8100
- required: false,
8101
- disabled: true,
8102
- system: true,
8103
- icon: "clock-circle",
8104
- description: "When this record was created"
8105
- },
8106
- updatedAt: {
8107
- id: "system:updatedAt",
8108
- name: "updatedAt",
8109
- label: "Last edited time",
8110
- type: "date",
8111
- required: false,
8112
- disabled: true,
8113
- system: true,
8114
- icon: "clock-circle",
8115
- description: "When this record was last modified"
8116
- },
8117
- createdBy: {
8118
- id: "system:createdBy",
8119
- name: "createdBy",
8120
- label: "Created by",
8121
- type: "user",
8122
- required: false,
8123
- disabled: true,
8124
- system: true,
8125
- multiple: false,
8126
- icon: "user",
8127
- description: "User who created this record"
8128
- },
8129
- lastUpdatedBy: {
8130
- id: "system:lastUpdatedBy",
8131
- name: "lastUpdatedBy",
8132
- label: "Last edited by",
8133
- type: "user",
8134
- required: false,
8135
- disabled: true,
8136
- system: true,
8137
- multiple: false,
8138
- icon: "user",
8139
- description: "User who last modified this record"
8140
- },
8141
- /**
8142
- * System attribute for free-form document attachments.
8143
- * Hidden from forms, visible in Documents tab.
8144
- * Users can attach any document without template constraints.
8145
- */
8146
- attachments: {
8147
- id: "system:attachments",
8148
- name: "attachments",
8149
- label: "Pi\xE8ces jointes",
8150
- type: "document",
8151
- required: false,
8152
- disabled: false,
8153
- // Editable via Documents tab
8154
- system: true,
8155
- hidden: true,
8156
- // Hidden from regular form views
8157
- multiple: true,
8158
- icon: "paperclip",
8159
- description: "Free-form document attachments"
8160
- // No templateId = all templates allowed
8161
- }
8162
- };
8163
- function getSystemAttributeList() {
8164
- return Object.values(SYSTEM_ATTRIBUTES);
8165
- }
8166
- function isSystemAttribute(name) {
8167
- return name in SYSTEM_ATTRIBUTES;
8168
- }
8169
- function isSystemAttributeObject(attr) {
8170
- return attr.system === true;
8171
- }
8172
-
8173
- // src/validation/validators.ts
8174
- import { z as z5 } from "zod";
8175
- var regexPatternCache = /* @__PURE__ */ new Map();
8176
- function getCachedRegex(pattern) {
8177
- let cached = regexPatternCache.get(pattern);
8178
- if (!cached) {
8179
- cached = new RegExp(pattern);
8180
- regexPatternCache.set(pattern, cached);
8181
- }
8182
- return cached;
8183
- }
8184
- var DEFAULT_VALIDATION_MESSAGES = {
8185
- required: (attr) => `${attr.label} is required`,
8186
- invalidType: (attr, expected) => `${attr.label} must be a ${expected}`,
8187
- minLength: (attr, min) => `${attr.label} must be at least ${min} characters`,
8188
- maxLength: (attr, max) => `${attr.label} must be at most ${max} characters`,
8189
- invalidPattern: (attr) => `${attr.label} format is invalid`,
8190
- minValue: (attr, min) => `${attr.label} must be at least ${min}`,
8191
- maxValue: (attr, max) => `${attr.label} must be at most ${max}`,
8192
- mustBeInteger: (attr) => `${attr.label} must be an integer`,
8193
- invalidDate: (attr) => `${attr.label} must be a valid date`,
8194
- invalidOption: (attr, options) => `${attr.label} must be one of: ${options.join(", ")}`,
8195
- invalidId: (attr) => `${attr.label} must be a valid ID`,
8196
- minItems: (attr, min) => `${attr.label} must have at least ${min} item${min > 1 ? "s" : ""}`,
8197
- maxItems: (attr, max) => `${attr.label} must have at most ${max} item${max > 1 ? "s" : ""}`,
8198
- invalidRichtext: (attr) => `${attr.label} must be valid rich text content`,
8199
- invalidPhone: (attr) => `${attr.label} must be a valid phone number`,
8200
- invalidCurrency: (attr) => `${attr.label} must be a valid currency value`,
8201
- invalidLocation: (attr) => `${attr.label} must be a valid location`
8202
- };
8203
- var baseConfigSchema = z5.object({
8204
- disabled: z5.boolean().optional(),
8205
- placeholder: z5.string().optional(),
8206
- description: z5.string().optional(),
8207
- defaultValue: z5.unknown().optional(),
8208
- icon: z5.string().optional(),
8209
- order: z5.number().int().optional(),
8210
- hidden: z5.boolean().optional(),
8211
- archived: z5.boolean().optional(),
8212
- deprecated: z5.boolean().optional(),
8213
- metadata: z5.record(z5.string(), z5.unknown()).optional()
8214
- });
8215
- var optionSchema = z5.object({
8216
- id: z5.string().min(1),
8217
- label: z5.string().min(1),
8218
- value: z5.string().min(1),
8219
- color: z5.string().optional(),
8220
- icon: z5.string().optional(),
8221
- description: z5.string().optional(),
8222
- group: z5.enum(["idle", "in_progress", "finished"]).optional()
8223
- });
8224
- var optionsArraySchema = z5.array(optionSchema).min(1).refine(
8225
- (options) => {
8226
- const values = options.map((o) => o.value);
8227
- return new Set(values).size === values.length;
8228
- },
8229
- { message: "Duplicate option values are not allowed" }
8230
- );
8231
- var relationTargetSchema = z5.object({
8232
- object: z5.string().min(1),
8233
- displayTemplate: z5.string().optional(),
8234
- filter: z5.record(z5.string(), z5.unknown()).optional()
8235
- });
8236
- var textConfigSchema = baseConfigSchema.extend({
8237
- minLength: z5.number().int().min(0).optional(),
8238
- maxLength: z5.number().int().min(1).optional(),
8239
- pattern: z5.string().optional()
8240
- });
8241
- var textareaConfigSchema = baseConfigSchema;
8242
- var richtextConfigSchema = baseConfigSchema.extend({
8243
- features: z5.array(
8244
- z5.enum(["headings", "bold", "italic", "lists", "links", "images", "codeBlocks", "tables"])
8245
- ).optional()
8246
- });
8247
- var numberConfigSchema = baseConfigSchema.extend({
8248
- min: z5.number().optional(),
8249
- max: z5.number().optional(),
8250
- unit: z5.enum(["integer", "decimal", "percentage"]).optional(),
8251
- decimals: z5.number().int().min(0).optional()
8252
- });
8253
- var checkboxConfigSchema = baseConfigSchema;
8254
- var dateConfigSchema = baseConfigSchema.extend({
8255
- dateFormat: z5.enum(["short", "long", "full", "relative"]).optional(),
8256
- minDate: z5.string().optional(),
8257
- maxDate: z5.string().optional()
8258
- });
8259
- var phoneConfigSchema = baseConfigSchema.extend({
8260
- defaultCountryCode: z5.string().length(3).optional()
8261
- });
8262
- var currencyConfigSchema = baseConfigSchema.extend({
8263
- defaultCurrency: z5.string().length(3).optional(),
8264
- allowedCurrencies: z5.array(z5.string().length(3)).optional()
8265
- });
8266
- var statusConfigSchema = baseConfigSchema.extend({
8267
- options: optionsArraySchema
8268
- });
8269
- var locationConfigSchema = baseConfigSchema.extend({
8270
- granularity: z5.enum(["full", "address", "city", "state", "country", "coordinates"]),
8271
- enableAutocomplete: z5.boolean().optional(),
8272
- enableMap: z5.boolean().optional(),
8273
- defaultCountry: z5.string().length(3).optional(),
8274
- allowedCountries: z5.array(z5.string().length(3)).optional(),
8275
- displayFormat: z5.enum(["single_line", "multi_line", "compact"]).optional()
8276
- });
8277
- var selectConfigSchema = baseConfigSchema.extend({
8278
- options: optionsArraySchema
8279
- });
8280
- var multiselectConfigSchema = baseConfigSchema.extend({
8281
- options: optionsArraySchema
8282
- });
8283
- var fileConfigSchema = baseConfigSchema.extend({
8284
- maxFiles: z5.number().int().min(1).optional(),
8285
- maxSize: z5.number().int().min(1).optional(),
8286
- allowedTypes: z5.array(z5.string()).optional(),
8287
- multiple: z5.boolean().optional()
8288
- });
8289
- var userConfigSchema = baseConfigSchema.extend({
8290
- allowedRoles: z5.array(z5.string()).optional(),
8291
- multiple: z5.boolean().optional()
8292
- });
8293
- var relationConfigSchema = baseConfigSchema.extend({
8294
- targets: z5.array(relationTargetSchema).min(1),
8295
- cardinality: z5.enum(["one", "many"]),
8296
- minItems: z5.number().int().min(0).optional(),
8297
- maxItems: z5.number().int().min(1).optional()
8298
- });
8299
- var ratingConfigSchema = baseConfigSchema.extend({
8300
- max: z5.number().int().min(1).optional(),
8301
- iconType: z5.enum(["star", "heart", "thumbs", "number"]).optional()
8302
- });
8303
- var formulaConfigSchema = baseConfigSchema.extend({
8304
- expression: z5.string().min(1),
8305
- returnType: z5.enum(["text", "number", "boolean", "date"]),
8306
- decimals: z5.number().int().min(0).max(10).optional(),
8307
- allowRelations: z5.boolean().optional()
8308
- });
8309
- var rollupConfigSchema = baseConfigSchema.extend({
8310
- relationAttribute: z5.string().min(1).optional(),
8311
- relationPath: z5.string().optional(),
8312
- targetAttribute: z5.string().min(1),
8313
- function: z5.enum([
8314
- // Numeric aggregates
8315
- "sum",
8316
- "avg",
8317
- // Date aggregates
8318
- "earliest",
8319
- "latest",
8320
- // Count (universal)
8321
- "count",
8322
- "countValues",
8323
- "countUniqueValues",
8324
- "countEmpty",
8325
- // Percent (universal)
8326
- "percentEmpty",
8327
- "percentNotEmpty",
8328
- // Lookup (universal)
8329
- "original"
8330
- ]),
8331
- decimals: z5.number().int().min(0).max(10).optional(),
8332
- targetAttributeType: z5.string().optional(),
8333
- targetAttributeOptions: z5.array(
8334
- z5.object({
8335
- id: z5.string(),
8336
- label: z5.string(),
8337
- value: z5.string(),
8338
- color: z5.string().optional(),
8339
- icon: z5.string().optional(),
8340
- description: z5.string().optional(),
8341
- group: z5.enum(["idle", "in_progress", "finished"]).optional()
8342
- })
8343
- ).optional()
8344
- });
8345
- var documentConfigSchema = baseConfigSchema.extend({
8346
- templateId: z5.string().optional(),
8347
- allowedTemplates: z5.array(z5.string()).optional(),
8348
- multiple: z5.boolean().optional(),
8349
- maxDocuments: z5.number().int().min(1).optional(),
8350
- autoProcess: z5.boolean().optional()
8351
- });
8352
- var attributeConfigSchemas = {
8353
- text: textConfigSchema,
8354
- textarea: textareaConfigSchema,
8355
- richtext: richtextConfigSchema,
8356
- number: numberConfigSchema,
8357
- checkbox: checkboxConfigSchema,
8358
- date: dateConfigSchema,
8359
- phone: phoneConfigSchema,
8360
- currency: currencyConfigSchema,
8361
- status: statusConfigSchema,
8362
- location: locationConfigSchema,
8363
- select: selectConfigSchema,
8364
- multiselect: multiselectConfigSchema,
8365
- file: fileConfigSchema,
8366
- user: userConfigSchema,
8367
- relation: relationConfigSchema,
8368
- rating: ratingConfigSchema,
8369
- formula: formulaConfigSchema,
8370
- rollup: rollupConfigSchema,
8371
- document: documentConfigSchema
8372
- };
8373
- function getAttributeConfigSchema(type) {
8374
- return attributeConfigSchemas[type];
8375
- }
8376
- function validateAttributeConfig(type, config) {
8377
- const schema = getAttributeConfigSchema(type);
8378
- const result = schema.safeParse(config);
8379
- if (result.success) {
8380
- return { success: true, data: result.data };
8381
- }
8382
- return {
8383
- success: false,
8384
- errors: result.error.issues.map((err) => `${err.path.join(".")}: ${err.message}`)
8385
- };
8386
- }
8387
- function parseAttributeConfig(type, config) {
8388
- const schema = getAttributeConfigSchema(type);
8389
- return schema.strip().parse(config);
8390
- }
8391
- function safeParseAttributeConfig(type, config) {
8392
- const schema = getAttributeConfigSchema(type);
8393
- const result = schema.strip().safeParse(config);
8394
- return result.success ? result.data : void 0;
8395
- }
8396
- function createTextValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8397
- let schema = z5.string();
8398
- if (attr.minLength !== void 0) {
8399
- schema = schema.min(attr.minLength, messages.minLength(attr, attr.minLength));
8400
- }
8401
- if (attr.maxLength !== void 0) {
8402
- schema = schema.max(attr.maxLength, messages.maxLength(attr, attr.maxLength));
8403
- }
8404
- if (attr.pattern) {
8405
- schema = schema.regex(getCachedRegex(attr.pattern), messages.invalidPattern(attr));
8406
- }
8407
- return schema;
8408
- }
8409
- function createNumberValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8410
- let schema = z5.number();
8411
- if (attr.min !== void 0) {
8412
- schema = schema.min(attr.min, messages.minValue(attr, attr.min));
8413
- }
8414
- if (attr.max !== void 0) {
8415
- schema = schema.max(attr.max, messages.maxValue(attr, attr.max));
8416
- }
8417
- if (attr.unit === "integer") {
8418
- schema = schema.int(messages.mustBeInteger(attr));
8419
- }
8420
- return schema;
8421
- }
8422
- function createCheckboxValidator(_attr, _messages = DEFAULT_VALIDATION_MESSAGES) {
8423
- return z5.boolean();
8424
- }
8425
- function createDateValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8426
- return z5.coerce.date({ message: messages.invalidDate(attr) });
8427
- }
8428
- function createPhoneValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8429
- return z5.object(
8430
- {
8431
- countryCode: z5.string().length(3),
8432
- phoneNumber: z5.string().min(1)
8433
- },
8434
- { message: messages.invalidPhone(attr) }
8435
- );
8436
- }
8437
- function createCurrencyValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8438
- return z5.object(
8439
- {
8440
- code: z5.string().length(3),
8441
- value: z5.number().min(0)
8442
- },
8443
- { message: messages.invalidCurrency(attr) }
8444
- );
8445
- }
8446
- function createStatusValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8447
- const validValues = attr.options.map((opt) => opt.value);
8448
- return z5.enum(validValues, {
8449
- message: messages.invalidOption(attr, validValues)
8450
- });
8451
- }
8452
- function createSelectValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8453
- const validValues = attr.options.map((opt) => opt.value);
8454
- return z5.enum(validValues, {
8455
- message: messages.invalidOption(attr, validValues)
8456
- });
8457
- }
8458
- function createMultiselectValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8459
- const validValues = attr.options.map((opt) => opt.value);
8460
- return z5.array(
8461
- z5.enum(validValues, {
8462
- message: messages.invalidOption(attr, validValues)
8463
- })
8464
- );
8465
- }
8466
- function createLocationValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8467
- return z5.object(
8468
- {
8469
- address: z5.string().optional(),
8470
- address2: z5.string().optional(),
8471
- city: z5.string().optional(),
8472
- state: z5.string().optional(),
8473
- postalCode: z5.string().optional(),
8474
- country: z5.string().length(3).optional(),
8475
- latitude: z5.number().optional(),
8476
- longitude: z5.number().optional()
8477
- },
8478
- { message: messages.invalidLocation(attr) }
8479
- );
8480
- }
8481
- function createFileValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8482
- const uuidSchema = z5.uuid({
8483
- message: messages.invalidId(attr)
8484
- });
8485
- if (attr.multiple) {
8486
- let arraySchema = z5.array(uuidSchema);
8487
- if (attr.maxFiles) {
8488
- arraySchema = arraySchema.max(attr.maxFiles, messages.maxItems(attr, attr.maxFiles));
8489
- }
8490
- return arraySchema;
8491
- }
8492
- return uuidSchema;
8493
- }
8494
- function createUserValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8495
- const uuidSchema = z5.uuid({
8496
- message: messages.invalidId(attr)
8497
- });
8498
- if (attr.multiple) {
8499
- return z5.array(uuidSchema);
8500
- }
8501
- return uuidSchema;
8502
- }
8503
- function createSingleRelationValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8504
- const uuidSchema = z5.uuid({
8505
- message: messages.invalidId(attr)
8506
- });
8507
- return z5.union([uuidSchema, z5.null()]);
8508
- }
8509
- function createMultiRelationValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8510
- const uuidSchema = z5.uuid({
8511
- message: messages.invalidId(attr)
8512
- });
8513
- let arraySchema = z5.array(uuidSchema);
8514
- if (attr.minItems !== void 0) {
8515
- arraySchema = arraySchema.min(attr.minItems, messages.minItems(attr, attr.minItems));
8516
- }
8517
- if (attr.maxItems !== void 0) {
8518
- arraySchema = arraySchema.max(attr.maxItems, messages.maxItems(attr, attr.maxItems));
8519
- }
8520
- return arraySchema;
8521
- }
8522
- function createRelationValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8523
- if (attr.cardinality === "many") {
8524
- return createMultiRelationValidator(attr, messages);
8525
- }
8526
- return createSingleRelationValidator(attr, messages);
8527
- }
8528
- function createRatingValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8529
- let schema = z5.number().min(0);
8530
- if (attr.max !== void 0) {
8531
- schema = schema.max(attr.max, messages.maxValue(attr, attr.max));
8532
- }
8533
- return schema;
8534
- }
8535
- function createFormulaValidator(_attr, _messages = DEFAULT_VALIDATION_MESSAGES) {
8536
- return z5.unknown();
8537
- }
8538
- function createRollupValidator(_attr, _messages = DEFAULT_VALIDATION_MESSAGES) {
8539
- return z5.unknown();
8540
- }
8541
- function createTextAreaValidator(_attr, _messages = DEFAULT_VALIDATION_MESSAGES) {
8542
- return z5.string();
8543
- }
8544
- function createRichtextValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8545
- return z5.string({
8546
- message: messages.invalidRichtext(attr)
8547
- });
8548
- }
8549
- function createAttributeValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8550
- switch (attr.type) {
8551
- case "text":
8552
- return createTextValidator(attr, messages);
8553
- case "textarea":
8554
- return createTextAreaValidator(attr, messages);
8555
- case "richtext":
8556
- return createRichtextValidator(attr, messages);
8557
- case "number":
8558
- return createNumberValidator(attr, messages);
8559
- case "checkbox":
8560
- return createCheckboxValidator(attr, messages);
8561
- case "date":
8562
- return createDateValidator(attr, messages);
8563
- case "phone":
8564
- return createPhoneValidator(attr, messages);
8565
- case "currency":
8566
- return createCurrencyValidator(attr, messages);
8567
- case "status":
8568
- return createStatusValidator(attr, messages);
8569
- case "location":
8570
- return createLocationValidator(attr, messages);
8571
- case "select":
8572
- return createSelectValidator(attr, messages);
8573
- case "multiselect":
8574
- return createMultiselectValidator(attr, messages);
8575
- case "file":
8576
- return createFileValidator(attr, messages);
8577
- case "user":
8578
- return createUserValidator(attr, messages);
8579
- case "relation":
8580
- return createRelationValidator(attr, messages);
8581
- case "rating":
8582
- return createRatingValidator(attr, messages);
8583
- case "formula":
8584
- return createFormulaValidator(attr, messages);
8585
- case "rollup":
8586
- return createRollupValidator(attr, messages);
8587
- default:
8588
- return z5.unknown();
8589
- }
8590
- }
8591
- function isEmptyValue(value) {
8592
- if (value === null || value === void 0) return true;
8593
- if (typeof value === "string" && value.trim() === "") return true;
8594
- if (value instanceof Date) return false;
8595
- if (typeof value === "object" && !Array.isArray(value)) {
8596
- return Object.values(value).every(
8597
- (v) => v === null || v === void 0 || typeof v === "string" && v.trim() === ""
8598
- );
8599
- }
8600
- return false;
8601
- }
8602
- function withEmptyToNull(validator) {
8603
- return z5.preprocess((val) => isEmptyValue(val) ? null : val, validator.nullish());
8604
- }
8605
- function createFormAttributeValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
8606
- const validator = createAttributeValidator(attr, messages);
8607
- if (!attr.required) {
8608
- return withEmptyToNull(validator);
8609
- }
8610
- return validator;
8611
- }
8612
- function createObjectValidator(objectDef) {
8613
- const shape = {};
8614
- for (const attr of objectDef.attributes) {
8615
- const validator = createAttributeValidator(attr);
8616
- shape[attr.name] = attr.required ? validator : withEmptyToNull(validator);
8617
- }
8618
- return z5.object(shape).passthrough();
8619
- }
8620
- function validateAttribute(attr, value) {
8621
- const validator = createAttributeValidator(attr);
8622
- if (!attr.required && (value === void 0 || value === null)) {
8623
- return { success: true, data: { [attr.name]: value } };
8624
- }
8625
- const result = validator.safeParse(value);
8626
- if (result.success) {
8627
- return {
8628
- success: true,
8629
- data: { [attr.name]: result.data }
8630
- };
8631
- }
8632
- return {
8633
- success: false,
8634
- errors: result.error.issues.map((err) => ({
8635
- path: [attr.name, ...err.path.map(String)],
8636
- message: err.message
8637
- }))
8638
- };
8639
- }
8640
- function validateObject(objectDef, data) {
8641
- const validator = createObjectValidator(objectDef);
8642
- const result = validator.safeParse(data);
8643
- if (result.success) {
8644
- return {
8645
- success: true,
8646
- data: result.data
8647
- };
8648
- }
8649
- return {
8650
- success: false,
8651
- errors: result.error.issues.map((err) => ({
8652
- path: err.path.map(String),
8653
- message: err.message
8654
- }))
8655
- };
8656
- }
8657
- function validateObjectOrThrow(objectDef, data) {
8658
- const result = validateObject(objectDef, data);
8659
- if (!result.success) {
8660
- const errorMessages = result.errors?.map((err) => `${err.path.join(".")}: ${err.message}`).join("\n") || "Unknown validation error";
8661
- throw new Error(`Validation failed for ${objectDef.label}:
8662
- ${errorMessages}`);
8663
- }
8664
- return result.data;
8665
- }
8666
- function createDraftValidator(objectDef) {
8667
- const shape = {};
8668
- for (const attr of objectDef.attributes) {
8669
- const validator = createAttributeValidator(attr);
8670
- shape[attr.name] = withEmptyToNull(validator);
8671
- }
8672
- return z5.object(shape).passthrough();
8673
- }
8674
- function validateDraft(objectDef, data) {
8675
- const validator = createDraftValidator(objectDef);
8676
- const result = validator.safeParse(data);
8677
- if (result.success) {
8678
- return {
8679
- success: true,
8680
- data: result.data
8681
- };
8682
- }
8683
- return {
8684
- success: false,
8685
- errors: result.error.issues.map((err) => ({
8686
- path: err.path.map(String),
8687
- message: err.message
8688
- }))
8689
- };
8690
- }
8691
- function validateDraftOrThrow(objectDef, data) {
8692
- const result = validateDraft(objectDef, data);
8693
- if (!result.success) {
8694
- const errorMessages = result.errors?.map((err) => `${err.path.join(".")}: ${err.message}`).join("\n") || "Unknown validation error";
8695
- throw new Error(`Draft validation failed for ${objectDef.label}:
8696
- ${errorMessages}`);
8697
- }
8698
- return result.data;
8699
- }
8700
- function isValuePresent(value) {
8701
- if (value === void 0 || value === null) {
8702
- return false;
8475
+ this.name = "AttributeInUseError";
8703
8476
  }
8704
- if (typeof value === "string" && value.trim() === "") {
8705
- return false;
8477
+ };
8478
+ var ObjectReferencedError = class extends Error {
8479
+ constructor(objectName, referencingObjects) {
8480
+ super(
8481
+ `Cannot delete object "${objectName}": target of relations in ${referencingObjects.join(", ")}`
8482
+ );
8483
+ this.objectName = objectName;
8484
+ this.referencingObjects = referencingObjects;
8485
+ this.code = "OBJECT_REFERENCED";
8486
+ this.name = "ObjectReferencedError";
8706
8487
  }
8707
- return true;
8708
- }
8709
- function getMissingRequiredAttributes(objectDef, data) {
8710
- const missing = [];
8711
- for (const attr of objectDef.attributes) {
8712
- if (attr.required && !isValuePresent(data[attr.name])) {
8713
- missing.push(attr);
8714
- }
8488
+ };
8489
+
8490
+ // src/types/system-attributes.ts
8491
+ var SYSTEM_ATTRIBUTES = {
8492
+ createdAt: {
8493
+ id: "system:createdAt",
8494
+ name: "createdAt",
8495
+ label: "Created time",
8496
+ type: "date",
8497
+ required: false,
8498
+ disabled: true,
8499
+ system: true,
8500
+ icon: "clock-circle",
8501
+ description: "When this record was created"
8502
+ },
8503
+ updatedAt: {
8504
+ id: "system:updatedAt",
8505
+ name: "updatedAt",
8506
+ label: "Last edited time",
8507
+ type: "date",
8508
+ required: false,
8509
+ disabled: true,
8510
+ system: true,
8511
+ icon: "clock-circle",
8512
+ description: "When this record was last modified"
8513
+ },
8514
+ createdBy: {
8515
+ id: "system:createdBy",
8516
+ name: "createdBy",
8517
+ label: "Created by",
8518
+ type: "user",
8519
+ required: false,
8520
+ disabled: true,
8521
+ system: true,
8522
+ multiple: false,
8523
+ icon: "user",
8524
+ description: "User who created this record"
8525
+ },
8526
+ lastUpdatedBy: {
8527
+ id: "system:lastUpdatedBy",
8528
+ name: "lastUpdatedBy",
8529
+ label: "Last edited by",
8530
+ type: "user",
8531
+ required: false,
8532
+ disabled: true,
8533
+ system: true,
8534
+ multiple: false,
8535
+ icon: "user",
8536
+ description: "User who last modified this record"
8537
+ },
8538
+ /**
8539
+ * System attribute for free-form document attachments.
8540
+ * Hidden from forms, visible in Documents tab.
8541
+ * Users can attach any document without template constraints.
8542
+ */
8543
+ attachments: {
8544
+ id: "system:attachments",
8545
+ name: "attachments",
8546
+ label: "Pi\xE8ces jointes",
8547
+ type: "document",
8548
+ required: false,
8549
+ disabled: false,
8550
+ // Editable via Documents tab
8551
+ system: true,
8552
+ hidden: true,
8553
+ // Hidden from regular form views
8554
+ multiple: true,
8555
+ icon: "paperclip",
8556
+ description: "Free-form document attachments"
8557
+ // No templateId = all templates allowed
8715
8558
  }
8716
- return missing;
8559
+ };
8560
+ function getSystemAttributeList() {
8561
+ return Object.values(SYSTEM_ATTRIBUTES);
8717
8562
  }
8718
- function isRecordComplete(objectDef, data) {
8719
- const missing = getMissingRequiredAttributes(objectDef, data);
8720
- if (missing.length > 0) {
8721
- return false;
8722
- }
8723
- const validation = validateObject(objectDef, data);
8724
- return validation.success;
8563
+ function isSystemAttribute(name) {
8564
+ return name in SYSTEM_ATTRIBUTES;
8725
8565
  }
8726
- function computeRecordStatus(objectDef, data) {
8727
- return isRecordComplete(objectDef, data) ? "complete" : "draft";
8566
+ function isSystemAttributeObject(attr) {
8567
+ return attr.system === true;
8728
8568
  }
8729
8569
 
8730
8570
  // src/runtime/services/audit/helpers.ts
@@ -10238,7 +10078,7 @@ async function preloadSchemas(records, schemaService) {
10238
10078
  if (records.length === 0) {
10239
10079
  return schemasByObjectId;
10240
10080
  }
10241
- const uniqueObjectIds = [...new Set(records.map((r) => r.objectId))];
10081
+ const uniqueObjectIds = [...records.reduce((set, r) => set.add(r.objectId), /* @__PURE__ */ new Set())];
10242
10082
  await Promise.all(
10243
10083
  uniqueObjectIds.map(async (objId) => {
10244
10084
  const schema = await schemaService.getObjectSchema(objId);
@@ -10387,6 +10227,13 @@ var RecordQueryService = class extends BaseService {
10387
10227
  effectiveTotal = exhausted ? collected.length : Math.max(collected.length, result.total);
10388
10228
  filteredRecords = collected.slice(requestedOffset, requestedOffset + requestedLimit);
10389
10229
  }
10230
+ if (options?.include && options.include.length > 0) {
10231
+ filteredRecords = await this.includeRelationsWithProperties(
10232
+ filteredRecords,
10233
+ schema,
10234
+ options.include
10235
+ );
10236
+ }
10390
10237
  if (!options?.skipFormulas) {
10391
10238
  return {
10392
10239
  records: enrichRecordsWithFormulas(filteredRecords, schema),
@@ -10462,6 +10309,79 @@ var RecordQueryService = class extends BaseService {
10462
10309
  }
10463
10310
  return result;
10464
10311
  }
10312
+ // ============================================================================
10313
+ // INCLUDE RELATIONS WITH PROPERTIES
10314
+ // ============================================================================
10315
+ /**
10316
+ * Include relation properties in records.
10317
+ *
10318
+ * For each requested relation attribute:
10319
+ * - If attribute has properties → Fetch from relation_attributes and return hybrid format
10320
+ * - If attribute has NO properties → Return legacy format (string[] or string)
10321
+ *
10322
+ * Uses batch loading to avoid N+1 queries.
10323
+ *
10324
+ * @param records - Records to enrich with relation properties
10325
+ * @param schema - Object schema
10326
+ * @param includes - Array of relation attribute names to include
10327
+ * @returns Records enriched with relation properties in hybrid format
10328
+ * @private
10329
+ */
10330
+ async includeRelationsWithProperties(records, schema, includes) {
10331
+ if (records.length === 0 || includes.length === 0) {
10332
+ return records;
10333
+ }
10334
+ for (const includeName of includes) {
10335
+ const attr = schema.attributes.find((a) => a.name === includeName);
10336
+ if (!attr || attr.type !== "relation") {
10337
+ continue;
10338
+ }
10339
+ if (attr.properties && this.adapter.relationAttributes) {
10340
+ const recordIds = records.map((r) => r.id);
10341
+ const relationAttributesRepo = this.adapter.relationAttributes;
10342
+ const allRelationProps = await Promise.all(
10343
+ recordIds.map(
10344
+ (recordId) => relationAttributesRepo.findBySource(schema.name, recordId, includeName)
10345
+ )
10346
+ );
10347
+ const propsByRecord = /* @__PURE__ */ new Map();
10348
+ allRelationProps.forEach((props, index) => {
10349
+ const recordId = recordIds[index];
10350
+ const propsMap = /* @__PURE__ */ new Map();
10351
+ for (const prop of props) {
10352
+ propsMap.set(prop.toId, prop.properties);
10353
+ }
10354
+ propsByRecord.set(recordId, propsMap);
10355
+ });
10356
+ for (const record of records) {
10357
+ const currentValue = record.values[includeName];
10358
+ const propsMap = propsByRecord.get(record.id);
10359
+ if (!currentValue) {
10360
+ continue;
10361
+ }
10362
+ if (!propsMap) {
10363
+ continue;
10364
+ }
10365
+ if (attr.cardinality === "many" && Array.isArray(currentValue)) {
10366
+ record.values[includeName] = currentValue.map((id) => {
10367
+ if (typeof id === "string") {
10368
+ const props = propsMap.get(id);
10369
+ return props ? { id, props } : { id };
10370
+ }
10371
+ return id;
10372
+ });
10373
+ } else if (attr.cardinality === "one") {
10374
+ const id = typeof currentValue === "string" ? currentValue : null;
10375
+ if (id) {
10376
+ const props = propsMap.get(id);
10377
+ record.values[includeName] = props ? { id, props } : { id };
10378
+ }
10379
+ }
10380
+ }
10381
+ }
10382
+ }
10383
+ return records;
10384
+ }
10465
10385
  };
10466
10386
 
10467
10387
  // src/runtime/services/record/record-resolver.service.ts
@@ -10556,6 +10476,280 @@ var RecordResolverService = class extends BaseService {
10556
10476
  }
10557
10477
  };
10558
10478
 
10479
+ // src/runtime/services/record/relation-properties.service.ts
10480
+ import { z as z5 } from "zod";
10481
+ var RelationPropertiesService = class extends BaseService {
10482
+ constructor(adapter) {
10483
+ super(adapter);
10484
+ }
10485
+ // ============================================================================
10486
+ // PUBLIC API
10487
+ // ============================================================================
10488
+ /**
10489
+ * Normalize relation values for storage in object_records table.
10490
+ *
10491
+ * Extracts IDs from hybrid format ({ id, props }) and returns legacy format (string[] or string).
10492
+ * This ensures object_records.values only contains IDs, while properties are in relation_attributes.
10493
+ *
10494
+ * @param schema - Object schema
10495
+ * @param data - Record data with hybrid relation values
10496
+ * @returns Data with relation values normalized to ID-only format
10497
+ */
10498
+ normalizeRelationValuesForStorage(schema, data) {
10499
+ const normalized = { ...data };
10500
+ for (const attr of schema.attributes) {
10501
+ if (attr.type !== "relation" || !attr.properties) {
10502
+ continue;
10503
+ }
10504
+ const value = data[attr.name];
10505
+ if (value === null || value === void 0) {
10506
+ continue;
10507
+ }
10508
+ if (attr.cardinality === "many" && Array.isArray(value)) {
10509
+ normalized[attr.name] = value.map((item) => {
10510
+ if (typeof item === "string") return item;
10511
+ if (typeof item === "object" && item !== null && "id" in item) {
10512
+ return item.id;
10513
+ }
10514
+ return item;
10515
+ });
10516
+ } else if (typeof value === "object" && value !== null && "id" in value) {
10517
+ normalized[attr.name] = value.id;
10518
+ }
10519
+ }
10520
+ return normalized;
10521
+ }
10522
+ /**
10523
+ * Synchronize relation properties for a given attribute.
10524
+ *
10525
+ * Handles:
10526
+ * - Format normalization (legacy → new)
10527
+ * - Validation of properties
10528
+ * - Upsert for present IDs
10529
+ * - Delete for absent IDs
10530
+ *
10531
+ * @param schema - Object schema
10532
+ * @param recordId - Source record ID
10533
+ * @param attributeName - Relation attribute name
10534
+ * @param relationValue - Relation value (hybrid format)
10535
+ * @param adapter - Database adapter
10536
+ */
10537
+ async syncRelationProperties(schema, recordId, attributeName, relationValue, adapter) {
10538
+ const attribute = schema.attributes.find((a) => a.name === attributeName);
10539
+ if (!attribute || attribute.type !== "relation") {
10540
+ return;
10541
+ }
10542
+ if (!attribute.properties) {
10543
+ return;
10544
+ }
10545
+ const normalized = this.normalizeRelationValue(relationValue);
10546
+ for (const item of normalized) {
10547
+ if (item.props) {
10548
+ this.validateProperties(attribute.properties, item.props);
10549
+ }
10550
+ }
10551
+ const existing = await adapter.relationAttributes?.findBySource(
10552
+ schema.name,
10553
+ recordId,
10554
+ attributeName
10555
+ );
10556
+ const existingIds = new Set((existing ?? []).map((r) => r.toId));
10557
+ const newIds = new Set(normalized.map((item) => item.id));
10558
+ const toUpsert = normalized.filter((item) => item.props !== void 0);
10559
+ const toDelete = Array.from(existingIds).filter((id) => !newIds.has(id));
10560
+ if (toUpsert.length > 0 && adapter.relationAttributes) {
10561
+ const inputs = toUpsert.map((item) => ({
10562
+ fromObject: schema.name,
10563
+ fromId: recordId,
10564
+ fromAttribute: attributeName,
10565
+ toId: item.id,
10566
+ properties: item.props ?? {},
10567
+ updatedBy: this.userId ?? void 0,
10568
+ createdBy: this.userId ?? void 0
10569
+ }));
10570
+ await adapter.relationAttributes.upsertBatch(inputs);
10571
+ }
10572
+ if (toDelete.length > 0 && adapter.relationAttributes && existing) {
10573
+ for (const toId of toDelete) {
10574
+ const relation2 = existing.find((r) => r.toId === toId);
10575
+ if (relation2) {
10576
+ }
10577
+ }
10578
+ await adapter.relationAttributes.deleteBySource(schema.name, recordId, attributeName);
10579
+ if (toUpsert.length > 0) {
10580
+ const inputs = toUpsert.map((item) => ({
10581
+ fromObject: schema.name,
10582
+ fromId: recordId,
10583
+ fromAttribute: attributeName,
10584
+ toId: item.id,
10585
+ properties: item.props ?? {},
10586
+ updatedBy: this.userId ?? void 0,
10587
+ createdBy: this.userId ?? void 0
10588
+ }));
10589
+ await adapter.relationAttributes.upsertBatch(inputs);
10590
+ }
10591
+ }
10592
+ }
10593
+ /**
10594
+ * Validate relation properties against PropertySchema.
10595
+ *
10596
+ * Uses Zod for runtime validation based on PropertyDefinition types.
10597
+ *
10598
+ * @param propertySchema - Schema defining allowed properties
10599
+ * @param properties - Properties to validate
10600
+ * @throws {z.ZodError} if validation fails
10601
+ */
10602
+ validateProperties(propertySchema, properties) {
10603
+ const schema = this.buildZodSchema(propertySchema);
10604
+ schema.parse(properties);
10605
+ }
10606
+ // ============================================================================
10607
+ // PRIVATE HELPERS
10608
+ // ============================================================================
10609
+ /**
10610
+ * Normalize relation value to unified internal format.
10611
+ *
10612
+ * Converts:
10613
+ * - string[] → Array<{ id, props?: undefined }>
10614
+ * - string → [{ id, props?: undefined }]
10615
+ * - null → []
10616
+ * - Array<{ id, props }> → Array<{ id, props }> (passthrough)
10617
+ * - { id, props } → [{ id, props }] (single to array)
10618
+ *
10619
+ * @param value - Relation value in hybrid format
10620
+ * @returns Normalized array of relation items
10621
+ * @private
10622
+ */
10623
+ normalizeRelationValue(value) {
10624
+ if (value === null || value === void 0) {
10625
+ return [];
10626
+ }
10627
+ if (typeof value === "string") {
10628
+ return [{ id: value }];
10629
+ }
10630
+ if (!Array.isArray(value) && typeof value === "object" && "id" in value) {
10631
+ return [value];
10632
+ }
10633
+ if (Array.isArray(value)) {
10634
+ return value.map((item) => {
10635
+ if (typeof item === "string") {
10636
+ return { id: item };
10637
+ }
10638
+ return item;
10639
+ });
10640
+ }
10641
+ return [];
10642
+ }
10643
+ /**
10644
+ * Build Zod schema from PropertySchema definition.
10645
+ *
10646
+ * Dynamically generates validation schema based on PropertyDefinition types.
10647
+ *
10648
+ * @param propertySchema - PropertySchema with definitions
10649
+ * @returns Zod schema for validation
10650
+ * @private
10651
+ */
10652
+ buildZodSchema(propertySchema) {
10653
+ const shape = {};
10654
+ for (const def of propertySchema.definitions) {
10655
+ let fieldSchema = this.buildFieldSchema(def);
10656
+ if (!def.required) {
10657
+ fieldSchema = fieldSchema.optional();
10658
+ }
10659
+ shape[def.name] = fieldSchema;
10660
+ }
10661
+ return z5.object(shape);
10662
+ }
10663
+ /**
10664
+ * Build Zod schema for a single property field.
10665
+ *
10666
+ * @param def - PropertyDefinition
10667
+ * @returns Zod schema for the field
10668
+ * @private
10669
+ */
10670
+ buildFieldSchema(def) {
10671
+ switch (def.type) {
10672
+ case "text":
10673
+ case "textarea": {
10674
+ let schema = z5.string();
10675
+ if (def.minLength !== void 0) {
10676
+ schema = schema.min(def.minLength);
10677
+ }
10678
+ if (def.maxLength !== void 0) {
10679
+ schema = schema.max(def.maxLength);
10680
+ }
10681
+ if (def.type === "text" && def.pattern) {
10682
+ schema = schema.regex(new RegExp(def.pattern));
10683
+ }
10684
+ return schema;
10685
+ }
10686
+ case "number": {
10687
+ let schema = z5.number();
10688
+ if (def.min !== void 0) {
10689
+ schema = schema.min(def.min);
10690
+ }
10691
+ if (def.max !== void 0) {
10692
+ schema = schema.max(def.max);
10693
+ }
10694
+ if (def.integer) {
10695
+ schema = schema.int();
10696
+ }
10697
+ return schema;
10698
+ }
10699
+ case "checkbox": {
10700
+ return z5.boolean();
10701
+ }
10702
+ case "date": {
10703
+ const schema = z5.string().datetime();
10704
+ return schema;
10705
+ }
10706
+ case "phone": {
10707
+ return z5.string();
10708
+ }
10709
+ case "currency": {
10710
+ let schema = z5.number();
10711
+ if (def.min !== void 0) {
10712
+ schema = schema.min(def.min);
10713
+ }
10714
+ if (def.max !== void 0) {
10715
+ schema = schema.max(def.max);
10716
+ }
10717
+ return schema;
10718
+ }
10719
+ case "status":
10720
+ case "select": {
10721
+ const validValues = def.options.map((opt) => opt.value);
10722
+ return z5.enum(validValues);
10723
+ }
10724
+ case "multiselect": {
10725
+ const validValues = def.options.map((opt) => opt.value);
10726
+ let schema = z5.array(z5.enum(validValues));
10727
+ if (def.maxSelections !== void 0) {
10728
+ schema = schema.max(def.maxSelections);
10729
+ }
10730
+ return schema;
10731
+ }
10732
+ case "rating": {
10733
+ let schema = z5.number().int();
10734
+ if (def.max !== void 0) {
10735
+ schema = schema.max(def.max);
10736
+ }
10737
+ return schema.min(0);
10738
+ }
10739
+ case "location": {
10740
+ return z5.object({
10741
+ address: z5.string().optional(),
10742
+ lat: z5.number().optional(),
10743
+ lng: z5.number().optional()
10744
+ });
10745
+ }
10746
+ default: {
10747
+ return z5.unknown();
10748
+ }
10749
+ }
10750
+ }
10751
+ };
10752
+
10559
10753
  // src/runtime/services/record/relation.service.ts
10560
10754
  var RelationService = class extends BaseService {
10561
10755
  constructor(adapter, nativeRegistry, options) {
@@ -10885,8 +11079,10 @@ var RelationService = class extends BaseService {
10885
11079
  const [attributeId, recordId] = c.split(":");
10886
11080
  return { compositeId: c, attributeId, recordId };
10887
11081
  });
10888
- const uniqueRecordIds = [...new Set(parsed.map((p) => p.recordId))];
10889
- const uniqueAttributeIds = [...new Set(parsed.map((p) => p.attributeId))];
11082
+ const uniqueRecordIds = [...parsed.reduce((set, p) => set.add(p.recordId), /* @__PURE__ */ new Set())];
11083
+ const uniqueAttributeIds = [
11084
+ ...parsed.reduce((set, p) => set.add(p.attributeId), /* @__PURE__ */ new Set())
11085
+ ];
10890
11086
  const records = await this.recordResolver.findByIds(uniqueRecordIds);
10891
11087
  if (records.length === 0) {
10892
11088
  return [];
@@ -10895,7 +11091,7 @@ var RelationService = class extends BaseService {
10895
11091
  const attributePromises = uniqueAttributeIds.map((id) => this.findAttributeById(id));
10896
11092
  const attributes = await Promise.all(attributePromises);
10897
11093
  const attributeMap = new Map(uniqueAttributeIds.map((id, i) => [id, attributes[i]]));
10898
- const uniqueObjectIds = [...new Set(records.map((r) => r.objectId))];
11094
+ const uniqueObjectIds = [...records.reduce((set, r) => set.add(r.objectId), /* @__PURE__ */ new Set())];
10899
11095
  const schemaPromises = uniqueObjectIds.map((id) => this.schemaService.getObjectSchema(id));
10900
11096
  const schemas = await Promise.all(schemaPromises);
10901
11097
  const schemaMap = new Map(uniqueObjectIds.map((id, i) => [id, schemas[i]]));
@@ -11148,7 +11344,10 @@ var RollupService = class extends BaseService {
11148
11344
  return values.filter((v) => v != null && v !== "").length;
11149
11345
  case "countUniqueValues": {
11150
11346
  const nonEmpty = values.filter((v) => v != null && v !== "");
11151
- return new Set(nonEmpty.map((v) => JSON.stringify(v))).size;
11347
+ return nonEmpty.reduce(
11348
+ (set, v) => set.add(JSON.stringify(v)),
11349
+ /* @__PURE__ */ new Set()
11350
+ ).size;
11152
11351
  }
11153
11352
  case "countEmpty":
11154
11353
  return values.filter((v) => v == null || v === "").length;
@@ -11379,6 +11578,7 @@ var RecordService = class extends BaseService {
11379
11578
  queryService: this.queryService,
11380
11579
  recordResolver: this.recordResolver
11381
11580
  });
11581
+ this.relationPropertiesService = new RelationPropertiesService(adapter);
11382
11582
  this.rollupService = new RollupService(adapter, {
11383
11583
  recordResolver: this.recordResolver
11384
11584
  });
@@ -11423,29 +11623,45 @@ var RecordService = class extends BaseService {
11423
11623
  if (!options?.skipHooks) {
11424
11624
  await this.hookRegistry.execute("beforeCreate", schema.name, hookCtx);
11425
11625
  }
11626
+ const normalizedData = this.relationPropertiesService.normalizeRelationValuesForStorage(
11627
+ schema,
11628
+ dataWithDefaults
11629
+ );
11426
11630
  if (options?.validate !== false) {
11427
11631
  if (options?.allowDraft) {
11428
- validateDraftOrThrow(schema, dataWithDefaults);
11632
+ validateDraftOrThrow(schema, normalizedData);
11429
11633
  } else {
11430
- validateObjectOrThrow(schema, dataWithDefaults);
11634
+ validateObjectOrThrow(schema, normalizedData);
11431
11635
  }
11432
11636
  if (!options?.skipRelationValidation) {
11433
- await this.relationService.validateRelationsOrThrow(schema, dataWithDefaults);
11637
+ await this.relationService.validateRelationsOrThrow(schema, normalizedData);
11434
11638
  }
11435
11639
  if (!options?.skipUserValidation) {
11436
- await this.userService.validateUsersOrThrow(schema, dataWithDefaults);
11640
+ await this.userService.validateUsersOrThrow(schema, normalizedData);
11437
11641
  }
11438
11642
  }
11439
- const completionStatus = computeRecordStatus(schema, dataWithDefaults);
11440
- const label = await computeLabel(schema, dataWithDefaults, this.labelResolver);
11643
+ const completionStatus = computeRecordStatus(schema, normalizedData);
11644
+ const label = await computeLabel(schema, normalizedData, this.labelResolver);
11441
11645
  const record = await this.adapter.objectRecords.create({
11442
11646
  objectId,
11443
- data: dataWithDefaults,
11647
+ data: normalizedData,
11444
11648
  label,
11445
11649
  completionStatus,
11446
11650
  metadata: options?.metadata,
11447
11651
  createdBy: this.userId
11448
11652
  });
11653
+ for (const [attrName, value] of Object.entries(dataWithDefaults)) {
11654
+ const attr = schema.attributes.find((a) => a.name === attrName);
11655
+ if (attr?.type === "relation" && attr.properties) {
11656
+ await this.relationPropertiesService.syncRelationProperties(
11657
+ schema,
11658
+ record.id,
11659
+ attrName,
11660
+ value,
11661
+ this.adapter
11662
+ );
11663
+ }
11664
+ }
11449
11665
  if (!options?.skipHooks) {
11450
11666
  const afterCtx = {
11451
11667
  ...hookCtx,
@@ -11576,30 +11792,29 @@ var RecordService = class extends BaseService {
11576
11792
  hookModifiedValues[key] = hookCtx.newValues[key];
11577
11793
  }
11578
11794
  }
11795
+ const dataToUpdate = { ...data, ...hookModifiedValues };
11796
+ const normalizedUpdate = this.relationPropertiesService.normalizeRelationValuesForStorage(
11797
+ schema,
11798
+ dataToUpdate
11799
+ );
11800
+ const normalizedMergedData = { ...existing.values, ...normalizedUpdate };
11579
11801
  if (options?.validate !== false) {
11580
11802
  if (options?.partial) {
11581
- validateDraftOrThrow(schema, mergedData);
11803
+ validateDraftOrThrow(schema, normalizedMergedData);
11582
11804
  } else {
11583
- validateObjectOrThrow(schema, mergedData);
11805
+ validateObjectOrThrow(schema, normalizedMergedData);
11584
11806
  }
11585
11807
  if (!options?.skipRelationValidation) {
11586
- await this.relationService.validateRelationsOrThrow(schema, {
11587
- ...data,
11588
- ...hookModifiedValues
11589
- });
11808
+ await this.relationService.validateRelationsOrThrow(schema, normalizedUpdate);
11590
11809
  }
11591
11810
  if (!options?.skipUserValidation) {
11592
- await this.userService.validateUsersOrThrow(schema, {
11593
- ...data,
11594
- ...hookModifiedValues
11595
- });
11811
+ await this.userService.validateUsersOrThrow(schema, normalizedUpdate);
11596
11812
  }
11597
11813
  }
11598
- const completionStatus = computeRecordStatus(schema, mergedData);
11599
- const label = await computeLabel(schema, mergedData, this.labelResolver);
11814
+ const completionStatus = computeRecordStatus(schema, normalizedMergedData);
11815
+ const label = await computeLabel(schema, normalizedMergedData, this.labelResolver);
11600
11816
  const updatePayload = {
11601
- ...data,
11602
- ...hookModifiedValues,
11817
+ ...normalizedUpdate,
11603
11818
  __completionStatus: completionStatus,
11604
11819
  __label: label,
11605
11820
  __lastUpdatedBy: this.userId,
@@ -11615,6 +11830,18 @@ var RecordService = class extends BaseService {
11615
11830
  }
11616
11831
  const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
11617
11832
  await this.invalidateRecordCaches(recordId, existing.objectId);
11833
+ for (const [attrName, value] of Object.entries(dataToUpdate)) {
11834
+ const attr = schema.attributes.find((a) => a.name === attrName);
11835
+ if (attr?.type === "relation" && attr.properties) {
11836
+ await this.relationPropertiesService.syncRelationProperties(
11837
+ schema,
11838
+ recordId,
11839
+ attrName,
11840
+ value,
11841
+ this.adapter
11842
+ );
11843
+ }
11844
+ }
11618
11845
  if (!options?.skipHooks) {
11619
11846
  const afterCtx = {
11620
11847
  ...hookCtx,
@@ -13987,7 +14214,7 @@ var WorkflowService = class extends BaseService {
13987
14214
  };
13988
14215
  const validationResult = WorkflowDefinitionSchema.safeParse(definition);
13989
14216
  if (!validationResult.success) {
13990
- const errors = validationResult.error.issues.map((i) => i.message);
14217
+ const errors = formatZodErrors(validationResult.error).map((err) => err.message);
13991
14218
  throw new SchemaError(
13992
14219
  `Invalid workflow definition: ${errors.join(", ")}`,
13993
14220
  SchemaErrorCode.VALIDATION_FAILED
@@ -14029,7 +14256,7 @@ var WorkflowService = class extends BaseService {
14029
14256
  };
14030
14257
  const validationResult = WorkflowDefinitionSchema.safeParse(updated);
14031
14258
  if (!validationResult.success) {
14032
- const errors = validationResult.error.issues.map((i) => i.message);
14259
+ const errors = formatZodErrors(validationResult.error).map((err) => err.message);
14033
14260
  throw new SchemaError(
14034
14261
  `Invalid workflow definition: ${errors.join(", ")}`,
14035
14262
  SchemaErrorCode.VALIDATION_FAILED
@@ -14058,7 +14285,7 @@ var WorkflowService = class extends BaseService {
14058
14285
  }
14059
14286
  const validationResult = WorkflowDefinitionSchema.safeParse(existing);
14060
14287
  if (!validationResult.success) {
14061
- const errors = validationResult.error.issues.map((i) => i.message);
14288
+ const errors = formatZodErrors(validationResult.error).map((err) => err.message);
14062
14289
  throw new SchemaError(
14063
14290
  `Cannot publish invalid workflow: ${errors.join(", ")}`,
14064
14291
  SchemaErrorCode.VALIDATION_FAILED
@@ -15143,7 +15370,7 @@ var DocumentService = class extends BaseService {
15143
15370
  const template = await this.templateService.getTemplateOrThrow(document2.templateId);
15144
15371
  const slots = await this.getSlots(documentId);
15145
15372
  const requiredSlots = template.slots.filter((s) => s.required);
15146
- const filledSlotNames = new Set(slots.map((s) => s.slotName));
15373
+ const filledSlotNames = slots.reduce((set, s) => set.add(s.slotName), /* @__PURE__ */ new Set());
15147
15374
  const allRequiredFilled = requiredSlots.every((s) => filledSlotNames.has(s.name));
15148
15375
  if (!allRequiredFilled) {
15149
15376
  return await this.updateStatus(documentId, "draft");
@@ -16717,7 +16944,7 @@ var PermissionService = class extends BaseService {
16717
16944
  DEFAULT_ROLE_PERMISSIONS
16718
16945
  } = await import("./default-roles-42X3TJI5.mjs");
16719
16946
  const existingRoles = await this.getRoles();
16720
- const existingRoleNames = new Set(existingRoles.map((r) => r.name));
16947
+ const existingRoleNames = existingRoles.reduce((set, r) => set.add(r.name), /* @__PURE__ */ new Set());
16721
16948
  for (const roleName of Object.values(DEFAULT_ROLES)) {
16722
16949
  if (existingRoleNames.has(roleName)) {
16723
16950
  continue;
@@ -17211,7 +17438,7 @@ var ViewService = class extends BaseService {
17211
17438
  */
17212
17439
  async hasUserCustomizations(viewId, userId) {
17213
17440
  const overlay = await this.adapter.viewOverlays.findByViewAndUser(viewId, userId);
17214
- return overlay !== null && Object.keys(overlay.configOverrides).length > 0;
17441
+ return overlay !== null && hasProperties(overlay.configOverrides);
17215
17442
  }
17216
17443
  // ============================================================================
17217
17444
  // OVERLAY MERGE LOGIC
@@ -17586,8 +17813,9 @@ async function handleDryRun(adapter, nativeObject, existingObject, result, optio
17586
17813
  if (options.verbose) {
17587
17814
  console.info(`[SyncService] Would ${isNew ? "create" : "update"} object: ${nativeObject.name}`);
17588
17815
  }
17816
+ const existingAttrs = existingObject ? await adapter.attributes.findByObjectId(existingObject.id) : [];
17589
17817
  for (const attr of nativeObject.attributes) {
17590
- const existingAttr = existingObject ? await adapter.attributes.findByObjectId(existingObject.id).then((attrs) => attrs.find((a) => a.name === attr.name)) : null;
17818
+ const existingAttr = existingAttrs.find((a) => a.name === attr.name) ?? null;
17591
17819
  if (existingAttr) {
17592
17820
  result.attributesUpdated++;
17593
17821
  } else {
@@ -17609,8 +17837,9 @@ async function upsertObject(adapter, nativeObject, _options) {
17609
17837
  });
17610
17838
  }
17611
17839
  async function syncAttributes(adapter, nativeObject, dbObject, existingObject, result) {
17840
+ const existingAttrs = existingObject ? await adapter.attributes.findByObjectId(dbObject.id) : [];
17612
17841
  for (const [index, attr] of nativeObject.attributes.entries()) {
17613
- const existingAttr = existingObject ? await adapter.attributes.findByObjectId(dbObject.id).then((attrs) => attrs.find((a) => a.name === attr.name)) : null;
17842
+ const existingAttr = existingAttrs.find((a) => a.name === attr.name) ?? null;
17614
17843
  await adapter.attributes.upsert({
17615
17844
  objectId: dbObject.id,
17616
17845
  name: attr.name,
@@ -17704,8 +17933,6 @@ export {
17704
17933
  isIdentityProperty,
17705
17934
  isBehaviorProperty,
17706
17935
  isPresentationProperty,
17707
- getPropertyProtectionLevel,
17708
- filterPropertiesByCategory,
17709
17936
  RELATION_TARGET_ANY,
17710
17937
  isUniversalRelation,
17711
17938
  RecordReferencedError,
@@ -17715,6 +17942,7 @@ export {
17715
17942
  SYSTEM_FIELD_NAMES,
17716
17943
  RESERVED_ATTRIBUTE_NAMES,
17717
17944
  PolicyViolationError,
17945
+ FORBIDDEN_PROPERTY_TYPES,
17718
17946
  SYSTEM_ATTRIBUTES,
17719
17947
  getSystemAttributeList,
17720
17948
  isSystemAttribute,
@@ -17734,8 +17962,6 @@ export {
17734
17962
  and,
17735
17963
  or,
17736
17964
  inValues,
17737
- isEmpty,
17738
- isNotEmpty,
17739
17965
  isWorkflowDefinition,
17740
17966
  isWorkflowPublished,
17741
17967
  isSystemWorkflow,
@@ -17754,7 +17980,6 @@ export {
17754
17980
  createEmptyContext,
17755
17981
  getContextValue,
17756
17982
  setContextValue,
17757
- mergeFormToSlot,
17758
17983
  DEFAULT_THEME,
17759
17984
  mergeWithDefaults,
17760
17985
  generateCssVariables,
@@ -17788,12 +18013,10 @@ export {
17788
18013
  WorkflowConfigSchema,
17789
18014
  WorkflowStatusSchema,
17790
18015
  WorkflowDefinitionSchema,
17791
- asTenantId,
17792
- asUserId,
17793
- generateId,
17794
- generatePrefixedId,
17795
- slugify,
17796
- generateTemplateName,
18016
+ isEmpty,
18017
+ isNotEmpty,
18018
+ toUndefinedIfEmpty,
18019
+ hasProperties,
17797
18020
  EMPTY_VALUE_PLACEHOLDER,
17798
18021
  formatAttributeValue,
17799
18022
  SchemaErrorCode,
@@ -17818,7 +18041,22 @@ export {
17818
18041
  RoleNotFoundError,
17819
18042
  isForbiddenError,
17820
18043
  ConcurrentModificationError,
17821
- isConcurrentModificationError,
18044
+ PropertySchemaBuilder,
18045
+ PropertyTypeBuilder,
18046
+ BasePropertyBuilder,
18047
+ TextPropertyBuilder,
18048
+ TextareaPropertyBuilder,
18049
+ NumberPropertyBuilder,
18050
+ CheckboxPropertyBuilder,
18051
+ DatePropertyBuilder,
18052
+ PhonePropertyBuilder,
18053
+ CurrencyPropertyBuilder,
18054
+ StatusPropertyBuilder,
18055
+ SelectPropertyBuilder,
18056
+ MultiselectPropertyBuilder,
18057
+ RatingPropertyBuilder,
18058
+ LocationPropertyBuilder,
18059
+ validatePropertyType,
17822
18060
  text,
17823
18061
  textarea,
17824
18062
  richtext,
@@ -17876,63 +18114,6 @@ export {
17876
18114
  SYSTEM_TEMPLATES,
17877
18115
  getSystemTemplate,
17878
18116
  isSystemTemplate,
17879
- DEFAULT_VALIDATION_MESSAGES,
17880
- textConfigSchema,
17881
- textareaConfigSchema,
17882
- richtextConfigSchema,
17883
- numberConfigSchema,
17884
- checkboxConfigSchema,
17885
- dateConfigSchema,
17886
- phoneConfigSchema,
17887
- currencyConfigSchema,
17888
- statusConfigSchema,
17889
- locationConfigSchema,
17890
- selectConfigSchema,
17891
- multiselectConfigSchema,
17892
- fileConfigSchema,
17893
- userConfigSchema,
17894
- relationConfigSchema,
17895
- ratingConfigSchema,
17896
- formulaConfigSchema,
17897
- rollupConfigSchema,
17898
- documentConfigSchema,
17899
- attributeConfigSchemas,
17900
- getAttributeConfigSchema,
17901
- validateAttributeConfig,
17902
- parseAttributeConfig,
17903
- safeParseAttributeConfig,
17904
- createTextValidator,
17905
- createNumberValidator,
17906
- createCheckboxValidator,
17907
- createDateValidator,
17908
- createPhoneValidator,
17909
- createCurrencyValidator,
17910
- createStatusValidator,
17911
- createSelectValidator,
17912
- createMultiselectValidator,
17913
- createLocationValidator,
17914
- createFileValidator,
17915
- createUserValidator,
17916
- createSingleRelationValidator,
17917
- createMultiRelationValidator,
17918
- createRelationValidator,
17919
- createRatingValidator,
17920
- createFormulaValidator,
17921
- createRollupValidator,
17922
- createTextAreaValidator,
17923
- createRichtextValidator,
17924
- createAttributeValidator,
17925
- createFormAttributeValidator,
17926
- createObjectValidator,
17927
- validateAttribute,
17928
- validateObject,
17929
- validateObjectOrThrow,
17930
- createDraftValidator,
17931
- validateDraft,
17932
- validateDraftOrThrow,
17933
- getMissingRequiredAttributes,
17934
- isRecordComplete,
17935
- computeRecordStatus,
17936
18117
  WorkflowJwtService,
17937
18118
  hashOptions,
17938
18119
  cacheKeys,
@@ -18045,6 +18226,7 @@ export {
18045
18226
  recalculateParentRollups,
18046
18227
  RecordQueryService,
18047
18228
  RecordResolverService,
18229
+ RelationPropertiesService,
18048
18230
  RelationService,
18049
18231
  RollupService,
18050
18232
  RecordService,