@stndrds/schema 1.0.0-alpha.72 → 1.0.0-alpha.74

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.
@@ -13,6 +13,9 @@ import {
13
13
  validateObject,
14
14
  validateObjectOrThrow
15
15
  } from "./chunk-5SZ5OISG.mjs";
16
+ import {
17
+ isAdminRole
18
+ } from "./chunk-6P3NPTYV.mjs";
16
19
  import {
17
20
  __require
18
21
  } from "./chunk-Y6FXYEAI.mjs";
@@ -2138,7 +2141,7 @@ var EndExecutor = class {
2138
2141
  this.nodeType = "end";
2139
2142
  }
2140
2143
  execute(node, _context) {
2141
- return complete(node.status ?? "completed");
2144
+ return complete(node.status || "completed");
2142
2145
  }
2143
2146
  canExecute(_node, _context) {
2144
2147
  return true;
@@ -2261,10 +2264,20 @@ var FormExecutor = class {
2261
2264
  const fieldRefs = this.collectFieldRefs(node);
2262
2265
  for (const fieldRef of fieldRefs) {
2263
2266
  const slot = slots.find((s) => s.id === fieldRef.slotId);
2264
- if (!slot) continue;
2267
+ if (!slot) {
2268
+ console.warn(
2269
+ `[FormExecutor] Slot "${fieldRef.slotId}" not found in workflow definition, skipping validation`
2270
+ );
2271
+ continue;
2272
+ }
2265
2273
  if (slot.mode === "select") continue;
2266
2274
  const object2 = objects.find((o) => o.name === slot.objectName);
2267
- if (!object2) continue;
2275
+ if (!object2) {
2276
+ console.warn(
2277
+ `[FormExecutor] Object "${slot.objectName}" not found for slot "${fieldRef.slotId}", skipping validation`
2278
+ );
2279
+ continue;
2280
+ }
2268
2281
  const attribute = object2.attributes.find((a) => a.name === fieldRef.attribute);
2269
2282
  if (!attribute?.required) continue;
2270
2283
  const slotInput = input[fieldRef.slotId];
@@ -2309,7 +2322,7 @@ var StartExecutor = class {
2309
2322
  }
2310
2323
  execute(node, _context) {
2311
2324
  if (!node.next) {
2312
- throw new Error(`StartNode "${node.id}" has no 'next' target defined`);
2325
+ return error("MISSING_NEXT", `StartNode "${node.id}" has no 'next' target defined`);
2313
2326
  }
2314
2327
  return success(node.next);
2315
2328
  }
@@ -2523,13 +2536,11 @@ function hasRelationReferences(expression) {
2523
2536
  return regex.test(expression);
2524
2537
  }
2525
2538
  function flattenRelationsForEval(resolvedRelations) {
2526
- const flat = {};
2539
+ const result = {};
2527
2540
  for (const [relationName, values] of Object.entries(resolvedRelations)) {
2528
- for (const [key, value] of Object.entries(values)) {
2529
- flat[`${relationName}.${key}`] = value;
2530
- }
2541
+ result[relationName] = { ...values };
2531
2542
  }
2532
- return flat;
2543
+ return result;
2533
2544
  }
2534
2545
  async function evaluateFormulaWithRelations(expression, record, schema, resolver) {
2535
2546
  const relationNames = extractRelationNames(expression);
@@ -2799,6 +2810,15 @@ function createMockAIConversationsRepository(stores) {
2799
2810
  },
2800
2811
  addMessage(input) {
2801
2812
  const now = /* @__PURE__ */ new Date();
2813
+ const conversation = stores.aiConversations.get(input.conversationId);
2814
+ if (!conversation) {
2815
+ return Promise.reject(new Error("Conversation not found"));
2816
+ }
2817
+ const tenantId = getTenantId();
2818
+ const userId = requireUserId();
2819
+ if (conversation.tenantId !== tenantId || conversation.userId !== userId) {
2820
+ return Promise.reject(new Error("Conversation not found or access denied"));
2821
+ }
2802
2822
  const message = {
2803
2823
  id: generateId(),
2804
2824
  conversationId: input.conversationId,
@@ -2816,17 +2836,23 @@ function createMockAIConversationsRepository(stores) {
2816
2836
  createdAt: now
2817
2837
  };
2818
2838
  stores.aiMessages.set(message.id, message);
2819
- const conversation = stores.aiConversations.get(input.conversationId);
2820
- if (conversation) {
2821
- conversation.messageCount++;
2822
- conversation.totalTokens += (input.inputTokens ?? 0) + (input.outputTokens ?? 0);
2823
- conversation.totalCost += input.cost ?? 0;
2824
- conversation.updatedAt = now;
2825
- stores.aiConversations.set(input.conversationId, conversation);
2826
- }
2839
+ conversation.messageCount++;
2840
+ conversation.totalTokens += (input.inputTokens ?? 0) + (input.outputTokens ?? 0);
2841
+ conversation.totalCost += input.cost ?? 0;
2842
+ conversation.updatedAt = now;
2843
+ stores.aiConversations.set(input.conversationId, conversation);
2827
2844
  return Promise.resolve(message);
2828
2845
  },
2829
2846
  listMessages(conversationId, options) {
2847
+ const conversation = stores.aiConversations.get(conversationId);
2848
+ if (!conversation) {
2849
+ return Promise.resolve({ messages: [], total: 0 });
2850
+ }
2851
+ const tenantId = getTenantId();
2852
+ const userId = requireUserId();
2853
+ if (conversation.tenantId !== tenantId || conversation.userId !== userId) {
2854
+ return Promise.resolve({ messages: [], total: 0 });
2855
+ }
2830
2856
  let results = Array.from(stores.aiMessages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
2831
2857
  const total = results.length;
2832
2858
  if (options?.limit) {
@@ -2835,6 +2861,15 @@ function createMockAIConversationsRepository(stores) {
2835
2861
  return Promise.resolve({ messages: results, total });
2836
2862
  },
2837
2863
  getRecentMessages(conversationId, count = 20) {
2864
+ const conversation = stores.aiConversations.get(conversationId);
2865
+ if (!conversation) {
2866
+ return Promise.resolve([]);
2867
+ }
2868
+ const tenantId = getTenantId();
2869
+ const userId = requireUserId();
2870
+ if (conversation.tenantId !== tenantId || conversation.userId !== userId) {
2871
+ return Promise.resolve([]);
2872
+ }
2838
2873
  const results = Array.from(stores.aiMessages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(-count);
2839
2874
  return Promise.resolve(results);
2840
2875
  }
@@ -3101,7 +3136,12 @@ function createMockFilesRepository(stores) {
3101
3136
  function createMockObjectsRepository(stores) {
3102
3137
  return {
3103
3138
  findById(id) {
3104
- return Promise.resolve(stores.objects.get(id) ?? null);
3139
+ const tenantId = getTenantId();
3140
+ const obj = stores.objects.get(id);
3141
+ if (!obj || obj.tenantId !== tenantId) {
3142
+ return Promise.resolve(null);
3143
+ }
3144
+ return Promise.resolve(obj);
3105
3145
  },
3106
3146
  findByName(name) {
3107
3147
  const tenantId = getTenantId();
@@ -3141,8 +3181,9 @@ function createMockObjectsRepository(stores) {
3141
3181
  return Promise.resolve(obj);
3142
3182
  },
3143
3183
  update(id, data) {
3184
+ const tenantId = getTenantId();
3144
3185
  const existing = stores.objects.get(id);
3145
- if (!existing) {
3186
+ if (!existing || existing.tenantId !== tenantId) {
3146
3187
  return Promise.reject(new Error(`Object ${id} not found`));
3147
3188
  }
3148
3189
  const updated = {
@@ -3154,6 +3195,11 @@ function createMockObjectsRepository(stores) {
3154
3195
  return Promise.resolve(updated);
3155
3196
  },
3156
3197
  delete(id) {
3198
+ const tenantId = getTenantId();
3199
+ const existing = stores.objects.get(id);
3200
+ if (!existing || existing.tenantId !== tenantId) {
3201
+ return Promise.reject(new Error(`Object ${id} not found`));
3202
+ }
3157
3203
  stores.objects.delete(id);
3158
3204
  return Promise.resolve();
3159
3205
  },
@@ -3167,7 +3213,7 @@ function createMockObjectsRepository(stores) {
3167
3213
  upsert(data) {
3168
3214
  const tenantId = getTenantId();
3169
3215
  for (const obj of stores.objects.values()) {
3170
- if (obj.system && obj.name === data.name) {
3216
+ if (obj.tenantId === tenantId && obj.system && obj.name === data.name) {
3171
3217
  const updated = {
3172
3218
  ...obj,
3173
3219
  label: data.label,
@@ -3700,13 +3746,24 @@ function parsePipeExpression(pipeExpr) {
3700
3746
  }
3701
3747
  function renderLabelExpression(template, values, fallback = DEFAULT_LABEL_FALLBACK) {
3702
3748
  const result = template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_, expr) => {
3703
- const parts = expr.split("|").map((s) => s.trim());
3704
- const path = parts[0];
3705
- let value = getValue(values, path);
3749
+ const orParts = expr.split("||").map((s) => s.trim());
3750
+ const lastPart = orParts[orParts.length - 1];
3751
+ const pipeSplit = lastPart.split("|").map((s) => s.trim());
3752
+ orParts[orParts.length - 1] = pipeSplit[0];
3753
+ const pipes = pipeSplit.slice(1).filter(Boolean);
3754
+ const alternatives = orParts.filter(Boolean);
3755
+ let value = "";
3756
+ for (const alt of alternatives) {
3757
+ const v = getValue(values, alt);
3758
+ if (v != null && v !== "") {
3759
+ value = v;
3760
+ break;
3761
+ }
3762
+ }
3706
3763
  const isEmpty3 = value == null || value === "";
3707
- if (isEmpty3 && parts.length === 1) return "";
3708
- for (let i = 1; i < parts.length; i++) {
3709
- const { name: pipeName, args } = parsePipeExpression(parts[i]);
3764
+ if (isEmpty3 && pipes.length === 0) return "";
3765
+ for (const pipeExpr of pipes) {
3766
+ const { name: pipeName, args } = parsePipeExpression(pipeExpr);
3710
3767
  const simpleFn = simplePipes[pipeName];
3711
3768
  if (simpleFn) {
3712
3769
  if (value != null && value !== "") {
@@ -3728,13 +3785,19 @@ function isLabelExpression(value) {
3728
3785
  }
3729
3786
  function extractAttributeNames(template) {
3730
3787
  const names = [];
3731
- const regex = /\{\{\s*([^|}]+)/g;
3788
+ const regex = /\{\{\s*([^}]+)\s*\}\}/g;
3732
3789
  let match;
3733
3790
  while ((match = regex.exec(template)) !== null) {
3734
- const path = match[1].trim();
3735
- const rootName = path.split(".")[0];
3736
- if (rootName && !names.includes(rootName)) {
3737
- names.push(rootName);
3791
+ const expr = match[1].trim();
3792
+ const orParts = expr.split("||").map((s) => s.trim());
3793
+ const lastPart = orParts[orParts.length - 1];
3794
+ orParts[orParts.length - 1] = lastPart.split("|")[0].trim();
3795
+ for (const part of orParts) {
3796
+ if (!part) continue;
3797
+ const rootName = part.split(".")[0];
3798
+ if (rootName && !names.includes(rootName)) {
3799
+ names.push(rootName);
3800
+ }
3738
3801
  }
3739
3802
  }
3740
3803
  return names;
@@ -3822,6 +3885,7 @@ function createMockObjectRecordsRepository(stores) {
3822
3885
  findById(id) {
3823
3886
  const internal = stores.objectRecords.get(id);
3824
3887
  if (!internal) return Promise.resolve(null);
3888
+ if (internal.deletedAt) return Promise.resolve(null);
3825
3889
  const { tenantId: _, ...record } = internal;
3826
3890
  return Promise.resolve(record);
3827
3891
  },
@@ -3829,7 +3893,7 @@ function createMockObjectRecordsRepository(stores) {
3829
3893
  const results = [];
3830
3894
  for (const id of ids) {
3831
3895
  const internal = stores.objectRecords.get(id);
3832
- if (internal) {
3896
+ if (internal && !internal.deletedAt) {
3833
3897
  const { tenantId: _, ...record } = internal;
3834
3898
  results.push(record);
3835
3899
  }
@@ -3890,13 +3954,18 @@ function createMockObjectRecordsRepository(stores) {
3890
3954
  return Promise.resolve(record);
3891
3955
  },
3892
3956
  delete(id) {
3893
- stores.objectRecords.delete(id);
3957
+ const existing = stores.objectRecords.get(id);
3958
+ if (!existing) {
3959
+ return Promise.reject(new Error(`ObjectRecord ${id} not found`));
3960
+ }
3961
+ existing.deletedAt = /* @__PURE__ */ new Date();
3962
+ stores.objectRecords.set(id, existing);
3894
3963
  return Promise.resolve();
3895
3964
  },
3896
3965
  list(objectId, options) {
3897
3966
  const tenantId = getTenantId();
3898
3967
  let results = Array.from(stores.objectRecords.values()).filter(
3899
- (r) => r.tenantId === tenantId && r.objectId === objectId
3968
+ (r) => r.tenantId === tenantId && r.objectId === objectId && !r.deletedAt
3900
3969
  );
3901
3970
  const total = results.length;
3902
3971
  if (options?.limit) {
@@ -3909,7 +3978,7 @@ function createMockObjectRecordsRepository(stores) {
3909
3978
  const tenantId = getTenantId();
3910
3979
  const lowerQuery = query.toLowerCase();
3911
3980
  let results = Array.from(stores.objectRecords.values()).filter((r) => {
3912
- if (r.tenantId !== tenantId || r.objectId !== objectId) return false;
3981
+ if (r.tenantId !== tenantId || r.objectId !== objectId || r.deletedAt) return false;
3913
3982
  return Object.values(r.values).some(
3914
3983
  (val) => String(val).toLowerCase().includes(lowerQuery)
3915
3984
  );
@@ -3943,7 +4012,7 @@ function createMockObjectRecordsRepository(stores) {
3943
4012
  }
3944
4013
  }
3945
4014
  let matchingRecords = Array.from(stores.objectRecords.values()).filter((r) => {
3946
- if (r.tenantId !== tenantId) return false;
4015
+ if (r.tenantId !== tenantId || r.deletedAt) return false;
3947
4016
  if (!allowedObjectIds.has(r.objectId)) return false;
3948
4017
  return Object.values(r.values).some(
3949
4018
  (val) => String(val).toLowerCase().includes(lowerQuery)
@@ -4098,12 +4167,12 @@ function createMockObjectRecordsRepository(stores) {
4098
4167
  }
4099
4168
  return Promise.resolve(updated);
4100
4169
  },
4101
- findByRelation(objectName, relationAttributeName, targetRecordId) {
4102
- const obj = Array.from(stores.objects.values()).find((o) => o.name === objectName);
4170
+ findByRelation(objectId, relationAttributeName, targetRecordId) {
4171
+ const obj = stores.objects.get(objectId);
4103
4172
  if (!obj) return Promise.resolve([]);
4104
4173
  const results = [];
4105
4174
  for (const record of stores.objectRecords.values()) {
4106
- if (record.objectId !== obj.id) continue;
4175
+ if (record.objectId !== objectId) continue;
4107
4176
  const value = record.values[relationAttributeName];
4108
4177
  let matches = false;
4109
4178
  if (typeof value === "string" && value === targetRecordId) {
@@ -4263,7 +4332,12 @@ function createEmptyStores() {
4263
4332
  function createMockUserProfilesRepository(stores) {
4264
4333
  return {
4265
4334
  findById(id) {
4266
- return Promise.resolve(stores.userProfiles.get(id) ?? null);
4335
+ const tenantId = getTenantId();
4336
+ const profile = stores.userProfiles.get(id);
4337
+ if (!profile || profile.tenantId !== tenantId) {
4338
+ return Promise.resolve(null);
4339
+ }
4340
+ return Promise.resolve(profile);
4267
4341
  },
4268
4342
  findByIds(ids) {
4269
4343
  const tenantId = getTenantId();
@@ -4277,8 +4351,9 @@ function createMockUserProfilesRepository(stores) {
4277
4351
  return Promise.resolve(results);
4278
4352
  },
4279
4353
  findByAuthId(authId) {
4354
+ const tenantId = getTenantId();
4280
4355
  for (const profile of stores.userProfiles.values()) {
4281
- if (profile.authId === authId) {
4356
+ if (profile.tenantId === tenantId && profile.authId === authId) {
4282
4357
  return Promise.resolve(profile);
4283
4358
  }
4284
4359
  }
@@ -4303,7 +4378,6 @@ function createMockUserProfilesRepository(stores) {
4303
4378
  firstName: data.firstName,
4304
4379
  lastName: data.lastName,
4305
4380
  avatarUrl: data.avatarUrl,
4306
- role: data.role ?? "member",
4307
4381
  status: data.status ?? "active",
4308
4382
  createdAt: /* @__PURE__ */ new Date(),
4309
4383
  updatedAt: /* @__PURE__ */ new Date()
@@ -4336,12 +4410,23 @@ function createMockUserProfilesRepository(stores) {
4336
4410
  }
4337
4411
  return Promise.resolve(results);
4338
4412
  },
4339
- countByRole(role) {
4413
+ getUsersWithRoles(filters) {
4340
4414
  const tenantId = getTenantId();
4341
- const count = Array.from(stores.userProfiles.values()).filter(
4342
- (profile) => profile.tenantId === tenantId && profile.role === role
4343
- ).length;
4344
- return Promise.resolve(count);
4415
+ const profiles = Array.from(stores.userProfiles.values()).filter(
4416
+ (p) => p.tenantId === tenantId
4417
+ );
4418
+ const results = [];
4419
+ for (const profile of profiles) {
4420
+ const roleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === profile.id && ur.tenantId === tenantId).map((ur) => ur.roleId);
4421
+ const roles = Array.from(stores.roles.values()).filter((r) => roleIds.includes(r.id));
4422
+ if (filters?.allowedRoles && filters.allowedRoles.length > 0) {
4423
+ const allowed = filters.allowedRoles;
4424
+ const hasMatchingRole = roles.some((r) => allowed.includes(r.name));
4425
+ if (!hasMatchingRole) continue;
4426
+ }
4427
+ results.push({ ...profile, roles });
4428
+ }
4429
+ return Promise.resolve(results);
4345
4430
  },
4346
4431
  updateLastLogin(id) {
4347
4432
  const profile = stores.userProfiles.get(id);
@@ -4360,7 +4445,6 @@ function createMockUserProfilesRepository(stores) {
4360
4445
  email: data.email,
4361
4446
  firstName: data.firstName,
4362
4447
  lastName: data.lastName,
4363
- role: data.role ?? "member",
4364
4448
  status: "pending",
4365
4449
  createdAt: /* @__PURE__ */ new Date(),
4366
4450
  updatedAt: /* @__PURE__ */ new Date()
@@ -4452,7 +4536,6 @@ function createMockPermissionsRepository(stores) {
4452
4536
  scope: input.scope,
4453
4537
  target: input.target,
4454
4538
  actions: input.actions,
4455
- filter: input.filter,
4456
4539
  createdAt: /* @__PURE__ */ new Date()
4457
4540
  };
4458
4541
  stores.permissions.set(perm.id, perm);
@@ -4497,7 +4580,7 @@ function createMockPermissionsRepository(stores) {
4497
4580
  const tenantId = getTenantId();
4498
4581
  const userRoleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
4499
4582
  const userRoles = Array.from(stores.roles.values()).filter((r) => userRoleIds.includes(r.id));
4500
- const isAdmin = userRoles.some((r) => r.name === "admin");
4583
+ const hasAdmin = userRoles.some((r) => isAdminRole(r.name));
4501
4584
  const userPermissions = Array.from(stores.permissions.values()).filter(
4502
4585
  (p) => userRoleIds.includes(p.roleId)
4503
4586
  );
@@ -4524,7 +4607,18 @@ function createMockPermissionsRepository(stores) {
4524
4607
  }
4525
4608
  }
4526
4609
  }
4527
- return Promise.resolve({ isAdmin, objectPermissions, systemPermissions });
4610
+ return Promise.resolve({ isAdmin: hasAdmin, objectPermissions, systemPermissions });
4611
+ },
4612
+ countUsersWithRole(roleName) {
4613
+ const tenantId = getTenantId();
4614
+ const role = Array.from(stores.roles.values()).find(
4615
+ (r) => r.tenantId === tenantId && r.name === roleName
4616
+ );
4617
+ if (!role) return Promise.resolve(0);
4618
+ const userIds = new Set(
4619
+ Array.from(stores.userRoles.values()).filter((ur) => ur.roleId === role.id && ur.tenantId === tenantId).map((ur) => ur.userProfileId)
4620
+ );
4621
+ return Promise.resolve(userIds.size);
4528
4622
  }
4529
4623
  };
4530
4624
  }
@@ -4847,6 +4941,10 @@ function createMockWorkflowsRepository(stores) {
4847
4941
  if (!existing) {
4848
4942
  return Promise.reject(new Error(`Workflow ${id} not found`));
4849
4943
  }
4944
+ const tenantId = getTenantId();
4945
+ if (existing.tenant_id !== tenantId && !existing.system) {
4946
+ return Promise.reject(new Error(`Workflow ${id} not found`));
4947
+ }
4850
4948
  const updated = {
4851
4949
  ...existing,
4852
4950
  label: data.label ?? existing.label,
@@ -4867,6 +4965,13 @@ function createMockWorkflowsRepository(stores) {
4867
4965
  return Promise.resolve(updated);
4868
4966
  },
4869
4967
  delete(id) {
4968
+ const existing = stores.workflows.get(id);
4969
+ if (existing) {
4970
+ const tenantId = getTenantId();
4971
+ if (existing.tenant_id !== tenantId && !existing.system) {
4972
+ return Promise.reject(new Error(`Workflow ${id} not found`));
4973
+ }
4974
+ }
4870
4975
  stores.workflows.delete(id);
4871
4976
  return Promise.resolve();
4872
4977
  },
@@ -4949,6 +5054,10 @@ function createMockWorkflowInstancesRepository(stores) {
4949
5054
  if (!existing) {
4950
5055
  return Promise.reject(new Error(`WorkflowInstance ${id} not found`));
4951
5056
  }
5057
+ const tenantId = getTenantId();
5058
+ if (existing.tenant_id !== tenantId) {
5059
+ return Promise.reject(new Error(`WorkflowInstance ${id} not found`));
5060
+ }
4952
5061
  const updated = {
4953
5062
  ...existing,
4954
5063
  status: data.status ?? existing.status,
@@ -5073,6 +5182,10 @@ function createMockWorkflowInvitationsRepository(stores) {
5073
5182
  if (!existing) {
5074
5183
  return Promise.reject(new Error(`WorkflowInvitation ${id} not found`));
5075
5184
  }
5185
+ const tenantId = getTenantId();
5186
+ if (existing.tenant_id !== tenantId) {
5187
+ return Promise.reject(new Error(`WorkflowInvitation ${id} not found`));
5188
+ }
5076
5189
  const updated = {
5077
5190
  ...existing,
5078
5191
  status: data.status ?? existing.status,
@@ -5138,6 +5251,10 @@ function createMockWorkflowAccessGrantsRepository(stores) {
5138
5251
  if (!existing) {
5139
5252
  return Promise.reject(new Error(`WorkflowAccessGrant ${id} not found`));
5140
5253
  }
5254
+ const tenantId = getTenantId();
5255
+ if (existing.tenant_id !== tenantId) {
5256
+ return Promise.reject(new Error(`WorkflowAccessGrant ${id} not found`));
5257
+ }
5141
5258
  const updated = {
5142
5259
  ...existing,
5143
5260
  last_used_at: data.lastUsedAt !== void 0 ? data.lastUsedAt.toISOString() : existing.last_used_at,
@@ -5271,6 +5388,20 @@ var PolicyRegistry = class {
5271
5388
  };
5272
5389
  var defaultPolicyRegistry = new PolicyRegistry();
5273
5390
 
5391
+ // src/runtime/search.ts
5392
+ var SORTABLE_ATTRIBUTE_TYPES = /* @__PURE__ */ new Set([
5393
+ "text",
5394
+ "textarea",
5395
+ "number",
5396
+ "date",
5397
+ "select",
5398
+ "status",
5399
+ "multiselect",
5400
+ "checkbox",
5401
+ "rating",
5402
+ "currency"
5403
+ ]);
5404
+
5274
5405
  // src/runtime/services/base.service.ts
5275
5406
  var BaseService = class {
5276
5407
  constructor(adapter) {
@@ -5490,6 +5621,15 @@ function isBilateralRelation(attr) {
5490
5621
  function inferInverseCardinality(cardinality) {
5491
5622
  return cardinality === "one" ? "many" : "many";
5492
5623
  }
5624
+ var NON_SORTABLE_TYPES = /* @__PURE__ */ new Set([
5625
+ "richtext",
5626
+ "file",
5627
+ "document",
5628
+ "location"
5629
+ ]);
5630
+ function isAttributeSortable(attr) {
5631
+ return !NON_SORTABLE_TYPES.has(attr.type);
5632
+ }
5493
5633
 
5494
5634
  // src/types/relation-properties.ts
5495
5635
  var FORBIDDEN_PROPERTY_TYPES = [
@@ -6209,6 +6349,20 @@ var SingleRelationAttributeBuilder = class extends BaseRelationAttributeBuilder
6209
6349
  isRequired: this.attr.required
6210
6350
  }
6211
6351
  );
6352
+ if (this.attr.system) multiBuilder.system();
6353
+ if (this.attr.hidden) multiBuilder.hidden();
6354
+ if (this.attr.disabled) multiBuilder.disabled();
6355
+ if (this.attr.placeholder) multiBuilder.placeholder(this.attr.placeholder);
6356
+ if (this.attr.description) multiBuilder.description(this.attr.description);
6357
+ if (this.attr.icon) multiBuilder.icon(this.attr.icon);
6358
+ if (this.attr.order !== void 0) multiBuilder.order(this.attr.order);
6359
+ if (this.attr.metadata) multiBuilder.metadata(this.attr.metadata);
6360
+ if (this.attr.featureGate) {
6361
+ multiBuilder.featureGate(this.attr.featureGate.flag, {
6362
+ expectedValue: this.attr.featureGate.expectedValue,
6363
+ fallback: this.attr.featureGate.fallback
6364
+ });
6365
+ }
6212
6366
  return multiBuilder;
6213
6367
  }
6214
6368
  required() {
@@ -6449,6 +6603,13 @@ var DocumentAttributeBuilder = class extends BaseAttributeBuilder {
6449
6603
  this.setRequired(true);
6450
6604
  return this;
6451
6605
  }
6606
+ /**
6607
+ * Mark this attribute as optional (undo required).
6608
+ */
6609
+ optional() {
6610
+ this.setRequired(false);
6611
+ return this;
6612
+ }
6452
6613
  };
6453
6614
  function document(config) {
6454
6615
  return new DocumentAttributeBuilder(config.name, config.label);
@@ -8901,6 +9062,19 @@ var ObjectSchemaService = class extends BaseService {
8901
9062
  });
8902
9063
  });
8903
9064
  await this.invalidateCachePattern(cacheKeys.allResolvedRelations(this.tenantId));
9065
+ if (this.adapter.search) {
9066
+ const { records } = await this.adapter.objectRecords.list(objectId, { limit: 1e4 });
9067
+ this.adapter.search.bulkIndex(
9068
+ records.map((r) => ({
9069
+ record: r,
9070
+ objectName: updatedDbObject.name,
9071
+ objectLabel: updatedDbObject.label,
9072
+ attributes
9073
+ }))
9074
+ ).catch(
9075
+ (err) => console.error("[search] Bulk re-index after label refresh failed", objectId, err)
9076
+ );
9077
+ }
8904
9078
  }
8905
9079
  return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
8906
9080
  }
@@ -10072,6 +10246,21 @@ var UserService = class extends BaseService {
10072
10246
  }
10073
10247
  const users = await this.adapter.userProfiles.findByIds([...allIds]);
10074
10248
  const userMap = new Map(users.map((u) => [u.id, u]));
10249
+ const allAllowedRoles = /* @__PURE__ */ new Set();
10250
+ for (const [, { attr }] of attrIdMap) {
10251
+ if (attr.allowedRoles && attr.allowedRoles.length > 0) {
10252
+ for (const role of attr.allowedRoles) {
10253
+ allAllowedRoles.add(role);
10254
+ }
10255
+ }
10256
+ }
10257
+ let userRolesMap;
10258
+ if (allAllowedRoles.size > 0) {
10259
+ const usersWithRoles = await this.adapter.userProfiles.getUsersWithRoles({
10260
+ allowedRoles: [...allAllowedRoles]
10261
+ });
10262
+ userRolesMap = new Map(usersWithRoles.map((u) => [u.id, u.roles.map((r) => r.name)]));
10263
+ }
10075
10264
  for (const [attrName, { attr, ids }] of attrIdMap) {
10076
10265
  const invalidIds = [];
10077
10266
  const roleErrors = [];
@@ -10085,8 +10274,10 @@ var UserService = class extends BaseService {
10085
10274
  invalidIds.push(id);
10086
10275
  continue;
10087
10276
  }
10088
- if (attr.allowedRoles && attr.allowedRoles.length > 0) {
10089
- if (!attr.allowedRoles.includes(user2.role)) {
10277
+ if (attr.allowedRoles && attr.allowedRoles.length > 0 && userRolesMap) {
10278
+ const userRoleNames = userRolesMap.get(id) ?? [];
10279
+ const hasAllowedRole = attr.allowedRoles.some((role) => userRoleNames.includes(role));
10280
+ if (!hasAllowedRole) {
10090
10281
  roleErrors.push(id);
10091
10282
  }
10092
10283
  }
@@ -10946,10 +11137,29 @@ var RecordQueryService = class extends BaseService {
10946
11137
  if (this.options?.permissionService && this.userId) {
10947
11138
  await checkPermission(this.options.permissionService, this.userId, schema.name, "read");
10948
11139
  }
10949
- const result = await runWithSchemaContext(
10950
- [schema],
10951
- () => this.adapter.objectRecords.search(objectId, query, options)
10952
- );
11140
+ let result;
11141
+ if (this.adapter.search) {
11142
+ try {
11143
+ result = await this.adapter.search.searchRecords(objectId, query, {
11144
+ limit: options?.limit,
11145
+ offset: options?.offset,
11146
+ sorts: options?.sorts,
11147
+ filters: options?.filters,
11148
+ attributes: schema.attributes
11149
+ });
11150
+ result = await this.healSearchResults(result);
11151
+ } catch {
11152
+ result = await runWithSchemaContext(
11153
+ [schema],
11154
+ () => this.adapter.objectRecords.search(objectId, query, options)
11155
+ );
11156
+ }
11157
+ } else {
11158
+ result = await runWithSchemaContext(
11159
+ [schema],
11160
+ () => this.adapter.objectRecords.search(objectId, query, options)
11161
+ );
11162
+ }
10953
11163
  const enrichedRecords = await this.relationPropertiesService.enrichRecordsBatch(
10954
11164
  result.records,
10955
11165
  schema
@@ -10965,6 +11175,32 @@ var RecordQueryService = class extends BaseService {
10965
11175
  total: result.total
10966
11176
  };
10967
11177
  }
11178
+ // ==========================================================================
11179
+ // Self-healing: ghost record cleanup
11180
+ // ==========================================================================
11181
+ /**
11182
+ * Verify Meilisearch results against PostgreSQL and remove ghost records.
11183
+ * Fire-and-forget cleanup of records that no longer exist in PG.
11184
+ */
11185
+ async healSearchResults(meiliResult) {
11186
+ const { search } = this.adapter;
11187
+ if (!search || meiliResult.records.length === 0) {
11188
+ return meiliResult;
11189
+ }
11190
+ const ids = meiliResult.records.map((r) => r.id);
11191
+ const existing = await this.adapter.objectRecords.findByIds(ids);
11192
+ const existingSet = new Set(existing.map((r) => r.id));
11193
+ const ghosts = ids.filter((id) => !existingSet.has(id));
11194
+ if (ghosts.length > 0) {
11195
+ Promise.all(ghosts.map((id) => search.removeRecord(id))).catch(
11196
+ (err) => console.error("[search] Failed to clean ghost records from per-object search", err)
11197
+ );
11198
+ }
11199
+ return {
11200
+ records: meiliResult.records.filter((r) => existingSet.has(r.id)),
11201
+ total: meiliResult.total - ghosts.length
11202
+ };
11203
+ }
10968
11204
  };
10969
11205
 
10970
11206
  // src/runtime/services/record/record-resolver.service.ts
@@ -11785,7 +12021,7 @@ var RollupService = class extends BaseService {
11785
12021
  if (typeof relatedId === "string" && relatedId.length > 0) {
11786
12022
  affectedIds.push(relatedId);
11787
12023
  } else if (Array.isArray(relatedId)) {
11788
- affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
12024
+ affectedIds.push(...relatedId.filter((id) => typeof id === "string" && id.length > 0));
11789
12025
  }
11790
12026
  }
11791
12027
  return [...new Set(affectedIds)];
@@ -12017,6 +12253,11 @@ var RecordService = class extends BaseService {
12017
12253
  }
12018
12254
  await recalculateParentRollups(record, schema, this.rollupContext);
12019
12255
  await this.invalidateRecordCaches(record.id, objectId);
12256
+ this.adapter.search?.indexRecord(record, {
12257
+ objectName: schema.name,
12258
+ objectLabel: schema.label,
12259
+ attributes: schema.attributes
12260
+ }).catch((err) => console.error("[search] Failed to index created record", record.id, err));
12020
12261
  if (this.auditService && this.userId) {
12021
12262
  this.auditService.logRecordAction({
12022
12263
  action: "record.created",
@@ -12180,6 +12421,11 @@ var RecordService = class extends BaseService {
12180
12421
  }
12181
12422
  const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
12182
12423
  await this.invalidateRecordCaches(recordId, existing.objectId);
12424
+ this.adapter.search?.indexRecord(updated, {
12425
+ objectName: schema.name,
12426
+ objectLabel: schema.label,
12427
+ attributes: schema.attributes
12428
+ }).catch((err) => console.error("[search] Failed to index updated record", updated.id, err));
12183
12429
  for (const [attrName, value] of Object.entries(dataToUpdate)) {
12184
12430
  const attr = schema.attributes.find((a) => a.name === attrName);
12185
12431
  if (attr?.type === "relation") {
@@ -12295,6 +12541,7 @@ var RecordService = class extends BaseService {
12295
12541
  }
12296
12542
  await this.adapter.objectRecords.delete(recordId);
12297
12543
  await this.invalidateRecordCaches(recordId, record.objectId);
12544
+ this.adapter.search?.removeRecord(recordId).catch((err) => console.error("[search] Failed to remove deleted record", recordId, err));
12298
12545
  if (!options?.skipHooks) {
12299
12546
  await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
12300
12547
  }
@@ -12374,6 +12621,11 @@ var RecordService = class extends BaseService {
12374
12621
  }
12375
12622
  const restored = await this.adapter.objectRecords.restore(recordId);
12376
12623
  await this.invalidateRecordCaches(recordId, record.objectId);
12624
+ this.adapter.search?.indexRecord(restored, {
12625
+ objectName: schema.name,
12626
+ objectLabel: schema.label,
12627
+ attributes: schema.attributes
12628
+ }).catch((err) => console.error("[search] Failed to index restored record", restored.id, err));
12377
12629
  if (!options?.skipHooks) {
12378
12630
  const afterCtx = {
12379
12631
  ...hookCtx,
@@ -14877,7 +15129,6 @@ var UserProfileService = class extends BaseService {
14877
15129
  * email: authUser.email,
14878
15130
  * firstName: authUser.user_metadata.first_name,
14879
15131
  * lastName: authUser.user_metadata.last_name,
14880
- * role: "member",
14881
15132
  * status: "active"
14882
15133
  * });
14883
15134
  * ```
@@ -14956,7 +15207,6 @@ var UserProfileService = class extends BaseService {
14956
15207
  * {
14957
15208
  * authId: authUser.id,
14958
15209
  * email: authUser.email,
14959
- * role: "member",
14960
15210
  * status: "active"
14961
15211
  * }
14962
15212
  * );
@@ -14981,7 +15231,6 @@ var UserProfileService = class extends BaseService {
14981
15231
  const changes = buildAuditChanges(existing, data, [
14982
15232
  "firstName",
14983
15233
  "lastName",
14984
- "role",
14985
15234
  "status"
14986
15235
  ]);
14987
15236
  if (changes.length > 0) {
@@ -15005,11 +15254,13 @@ var UserProfileService = class extends BaseService {
15005
15254
  */
15006
15255
  async deleteProfile(profileId, options) {
15007
15256
  const profile = await this.getProfileOrThrow(profileId);
15008
- if (options?.checkAdmin) {
15009
- if (profile.role === "admin") {
15010
- const adminCount = await this.adapter.userProfiles.countByRole("admin");
15011
- if (adminCount <= 1) {
15012
- throw new Error("Cannot delete the last admin user");
15257
+ if (options?.checkAdmin && this.adapter.permissions) {
15258
+ const ownerCount = await this.adapter.permissions.countUsersWithRole("owner");
15259
+ if (ownerCount <= 1) {
15260
+ const userRoles = await this.adapter.permissions.getUserRoles(profileId);
15261
+ const isOwner = userRoles.some((r) => r.name === "owner");
15262
+ if (isOwner) {
15263
+ throw new Error("Cannot delete the last owner user");
15013
15264
  }
15014
15265
  }
15015
15266
  }
@@ -15045,15 +15296,6 @@ var UserProfileService = class extends BaseService {
15045
15296
  async updateLastLogin(profileId) {
15046
15297
  await this.adapter.userProfiles.updateLastLogin(profileId);
15047
15298
  }
15048
- /**
15049
- * Change user role
15050
- *
15051
- * @param profileId - Profile UUID
15052
- * @param newRole - New role
15053
- */
15054
- async changeRole(profileId, newRole) {
15055
- return await this.updateProfile(profileId, { role: newRole });
15056
- }
15057
15299
  /**
15058
15300
  * Change user status
15059
15301
  *
@@ -15075,19 +15317,6 @@ var UserProfileService = class extends BaseService {
15075
15317
  () => this.adapter.userProfiles.findByEmail(email)
15076
15318
  );
15077
15319
  }
15078
- /**
15079
- * Check if user has role
15080
- */
15081
- async hasRole(profileId, role) {
15082
- const profile = await this.getProfile(profileId);
15083
- return profile?.role === role;
15084
- }
15085
- /**
15086
- * Check if user is admin
15087
- */
15088
- async isAdmin(profileId) {
15089
- return await this.hasRole(profileId, "admin");
15090
- }
15091
15320
  /**
15092
15321
  * Invite a new user by email.
15093
15322
  *
@@ -15108,7 +15337,6 @@ var UserProfileService = class extends BaseService {
15108
15337
  * email: "john@example.com",
15109
15338
  * firstName: "John",
15110
15339
  * lastName: "Doe",
15111
- * role: "member",
15112
15340
  * redirectTo: "https://app.example.com/welcome",
15113
15341
  * });
15114
15342
  * // Email sent automatically, profile.status === "pending"
@@ -16967,16 +17195,24 @@ var GlobalSearchService = class extends BaseService {
16967
17195
  if (!query || query.trim().length === 0) {
16968
17196
  return { results: [], total: 0 };
16969
17197
  }
16970
- return this.cachedList(
16971
- "globalSearch",
16972
- "search",
16973
- { query: query.trim(), ...options },
16974
- () => this.adapter.objectRecords.globalSearch(query.trim(), {
16975
- limit: options?.limit ?? 20,
16976
- offset: options?.offset ?? 0,
16977
- objectNames: options?.objectNames
16978
- })
16979
- );
17198
+ const trimmed = query.trim();
17199
+ if (this.adapter.search) {
17200
+ try {
17201
+ const raw = await this.adapter.search.globalSearch(trimmed, {
17202
+ objectNames: options?.objectNames,
17203
+ limit: options?.limit ?? 20,
17204
+ offset: options?.offset ?? 0
17205
+ });
17206
+ return await this.healGlobalSearchResults(raw);
17207
+ } catch (err) {
17208
+ console.error("[search] External search failed, falling back to PostgreSQL", err);
17209
+ }
17210
+ }
17211
+ return this.adapter.objectRecords.globalSearch(trimmed, {
17212
+ limit: options?.limit ?? 20,
17213
+ offset: options?.offset ?? 0,
17214
+ objectNames: options?.objectNames
17215
+ });
16980
17216
  }
16981
17217
  /**
16982
17218
  * Search and group results by object type.
@@ -16990,15 +17226,72 @@ var GlobalSearchService = class extends BaseService {
16990
17226
  if (!query || query.trim().length === 0) {
16991
17227
  return { groups: [], total: 0 };
16992
17228
  }
16993
- return this.cachedList(
16994
- "globalSearch",
16995
- "grouped",
16996
- { query: query.trim(), ...options },
16997
- () => this.adapter.objectRecords.globalSearchGrouped(query.trim(), {
16998
- objectNames: options?.objectNames,
16999
- limitPerGroup: options?.limitPerGroup ?? 5
17000
- })
17001
- );
17229
+ const trimmed = query.trim();
17230
+ if (this.adapter.search) {
17231
+ try {
17232
+ const raw = await this.adapter.search.globalSearchGrouped(trimmed, {
17233
+ objectNames: options?.objectNames
17234
+ });
17235
+ return await this.healGroupedSearchResults(raw);
17236
+ } catch (err) {
17237
+ console.error("[search] External grouped search failed, falling back to PostgreSQL", err);
17238
+ }
17239
+ }
17240
+ return this.adapter.objectRecords.globalSearchGrouped(trimmed, {
17241
+ objectNames: options?.objectNames,
17242
+ limitPerGroup: options?.limitPerGroup ?? 5
17243
+ });
17244
+ }
17245
+ // ==========================================================================
17246
+ // Self-healing: ghost record cleanup
17247
+ // ==========================================================================
17248
+ /**
17249
+ * Verify Meilisearch results against PostgreSQL and remove ghost records.
17250
+ * GlobalSearchResultItem uses `recordId` (not `id`) as the record identifier.
17251
+ */
17252
+ async healGlobalSearchResults(meiliResult) {
17253
+ const { search } = this.adapter;
17254
+ if (!search || meiliResult.results.length === 0) {
17255
+ return meiliResult;
17256
+ }
17257
+ const ids = meiliResult.results.map((r) => r.recordId);
17258
+ const existing = await this.adapter.objectRecords.findByIds(ids);
17259
+ const existingSet = new Set(existing.map((r) => r.id));
17260
+ const ghosts = ids.filter((id) => !existingSet.has(id));
17261
+ if (ghosts.length > 0) {
17262
+ Promise.all(ghosts.map((id) => search.removeRecord(id))).catch(
17263
+ (err) => console.error("[search] Failed to clean ghost records", err)
17264
+ );
17265
+ }
17266
+ return {
17267
+ results: meiliResult.results.filter((r) => existingSet.has(r.recordId)),
17268
+ total: meiliResult.total - ghosts.length
17269
+ };
17270
+ }
17271
+ /**
17272
+ * Verify grouped Meilisearch results against PostgreSQL and remove ghost records.
17273
+ * Filters ghosts from each group and removes empty groups.
17274
+ */
17275
+ async healGroupedSearchResults(raw) {
17276
+ const { search } = this.adapter;
17277
+ if (!search) return raw;
17278
+ const allIds = raw.groups.flatMap((g) => g.results.map((r) => r.recordId));
17279
+ if (allIds.length === 0) return raw;
17280
+ const existing = await this.adapter.objectRecords.findByIds(allIds);
17281
+ const existingSet = new Set(existing.map((r) => r.id));
17282
+ const ghosts = allIds.filter((id) => !existingSet.has(id));
17283
+ if (ghosts.length > 0) {
17284
+ Promise.all(ghosts.map((id) => search.removeRecord(id))).catch(
17285
+ (err) => console.error("[search] Failed to clean ghost records", err)
17286
+ );
17287
+ }
17288
+ return {
17289
+ groups: raw.groups.map((g) => {
17290
+ const filtered = g.results.filter((r) => existingSet.has(r.recordId));
17291
+ return { ...g, results: filtered, totalInGroup: filtered.length };
17292
+ }).filter((g) => g.results.length > 0),
17293
+ total: raw.total - ghosts.length
17294
+ };
17002
17295
  }
17003
17296
  };
17004
17297
 
@@ -17348,7 +17641,7 @@ var PermissionService = class extends BaseService {
17348
17641
  DEFAULT_ROLE_LABELS,
17349
17642
  DEFAULT_ROLE_DESCRIPTIONS,
17350
17643
  DEFAULT_ROLE_PERMISSIONS
17351
- } = await import("./default-roles-42X3TJI5.mjs");
17644
+ } = await import("./default-roles-OAMHLOCS.mjs");
17352
17645
  const existingRoles = await this.getRoles();
17353
17646
  const existingRoleNames = existingRoles.reduce((set, r) => set.add(r.name), /* @__PURE__ */ new Set());
17354
17647
  for (const roleName of Object.values(DEFAULT_ROLES)) {
@@ -18186,7 +18479,7 @@ async function syncSingleObject(adapter, nativeObject, result, options) {
18186
18479
  `Cannot sync shared object "${nativeObject.name}": masterTenantId must be configured in tenant options`
18187
18480
  );
18188
18481
  }
18189
- if (options.tenantId && options.tenantId !== options.masterTenantId) {
18482
+ if (!options.tenantId || options.tenantId !== options.masterTenantId) {
18190
18483
  throw new Error(
18191
18484
  `Cannot sync shared object "${nativeObject.name}": only master tenant "${options.masterTenantId}" can sync shared objects (current: "${options.tenantId}")`
18192
18485
  );
@@ -18194,12 +18487,13 @@ async function syncSingleObject(adapter, nativeObject, result, options) {
18194
18487
  }
18195
18488
  const existingObject = await adapter.objects.findSystemByName(nativeObject.name);
18196
18489
  const isNew = !existingObject;
18197
- updateObjectStats(result, isNew);
18198
18490
  if (options.dryRun) {
18491
+ updateObjectStats(result, isNew);
18199
18492
  await handleDryRun(adapter, nativeObject, existingObject, result, options, isNew);
18200
18493
  return;
18201
18494
  }
18202
18495
  const dbObject = await upsertObject(adapter, nativeObject, options);
18496
+ updateObjectStats(result, isNew);
18203
18497
  await syncAttributes(adapter, nativeObject, dbObject, existingObject, result);
18204
18498
  if (!isNew) {
18205
18499
  await cleanupAttributes(adapter, dbObject.id, nativeObject, result);
@@ -18228,6 +18522,11 @@ async function handleDryRun(adapter, nativeObject, existingObject, result, optio
18228
18522
  }
18229
18523
  result.attributesSynced++;
18230
18524
  }
18525
+ const codeAttributeNames = nativeObject.attributes.map((attr) => attr.name);
18526
+ const wouldBeDeleted = existingAttrs.filter(
18527
+ (a) => a.system && !codeAttributeNames.includes(a.name)
18528
+ );
18529
+ result.attributesDeleted += wouldBeDeleted.length;
18231
18530
  }
18232
18531
  async function upsertObject(adapter, nativeObject, _options) {
18233
18532
  return await adapter.objects.upsert({
@@ -18342,6 +18641,8 @@ export {
18342
18641
  isUniversalRelation,
18343
18642
  isBilateralRelation,
18344
18643
  inferInverseCardinality,
18644
+ NON_SORTABLE_TYPES,
18645
+ isAttributeSortable,
18345
18646
  RecordReferencedError,
18346
18647
  AttributeInUseError,
18347
18648
  ObjectReferencedError,
@@ -18593,6 +18894,7 @@ export {
18593
18894
  createMockAdapter,
18594
18895
  PolicyRegistry,
18595
18896
  defaultPolicyRegistry,
18897
+ SORTABLE_ATTRIBUTE_TYPES,
18596
18898
  BaseService,
18597
18899
  BaseRepository,
18598
18900
  SchemaContextAwareRepository,