@stndrds/schema 1.0.0-alpha.73 → 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.
@@ -2141,7 +2141,7 @@ var EndExecutor = class {
2141
2141
  this.nodeType = "end";
2142
2142
  }
2143
2143
  execute(node, _context) {
2144
- return complete(node.status ?? "completed");
2144
+ return complete(node.status || "completed");
2145
2145
  }
2146
2146
  canExecute(_node, _context) {
2147
2147
  return true;
@@ -2264,10 +2264,20 @@ var FormExecutor = class {
2264
2264
  const fieldRefs = this.collectFieldRefs(node);
2265
2265
  for (const fieldRef of fieldRefs) {
2266
2266
  const slot = slots.find((s) => s.id === fieldRef.slotId);
2267
- 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
+ }
2268
2273
  if (slot.mode === "select") continue;
2269
2274
  const object2 = objects.find((o) => o.name === slot.objectName);
2270
- 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
+ }
2271
2281
  const attribute = object2.attributes.find((a) => a.name === fieldRef.attribute);
2272
2282
  if (!attribute?.required) continue;
2273
2283
  const slotInput = input[fieldRef.slotId];
@@ -2312,7 +2322,7 @@ var StartExecutor = class {
2312
2322
  }
2313
2323
  execute(node, _context) {
2314
2324
  if (!node.next) {
2315
- throw new Error(`StartNode "${node.id}" has no 'next' target defined`);
2325
+ return error("MISSING_NEXT", `StartNode "${node.id}" has no 'next' target defined`);
2316
2326
  }
2317
2327
  return success(node.next);
2318
2328
  }
@@ -2526,13 +2536,11 @@ function hasRelationReferences(expression) {
2526
2536
  return regex.test(expression);
2527
2537
  }
2528
2538
  function flattenRelationsForEval(resolvedRelations) {
2529
- const flat = {};
2539
+ const result = {};
2530
2540
  for (const [relationName, values] of Object.entries(resolvedRelations)) {
2531
- for (const [key, value] of Object.entries(values)) {
2532
- flat[`${relationName}.${key}`] = value;
2533
- }
2541
+ result[relationName] = { ...values };
2534
2542
  }
2535
- return flat;
2543
+ return result;
2536
2544
  }
2537
2545
  async function evaluateFormulaWithRelations(expression, record, schema, resolver) {
2538
2546
  const relationNames = extractRelationNames(expression);
@@ -2802,6 +2810,15 @@ function createMockAIConversationsRepository(stores) {
2802
2810
  },
2803
2811
  addMessage(input) {
2804
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
+ }
2805
2822
  const message = {
2806
2823
  id: generateId(),
2807
2824
  conversationId: input.conversationId,
@@ -2819,17 +2836,23 @@ function createMockAIConversationsRepository(stores) {
2819
2836
  createdAt: now
2820
2837
  };
2821
2838
  stores.aiMessages.set(message.id, message);
2822
- const conversation = stores.aiConversations.get(input.conversationId);
2823
- if (conversation) {
2824
- conversation.messageCount++;
2825
- conversation.totalTokens += (input.inputTokens ?? 0) + (input.outputTokens ?? 0);
2826
- conversation.totalCost += input.cost ?? 0;
2827
- conversation.updatedAt = now;
2828
- stores.aiConversations.set(input.conversationId, conversation);
2829
- }
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);
2830
2844
  return Promise.resolve(message);
2831
2845
  },
2832
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
+ }
2833
2856
  let results = Array.from(stores.aiMessages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
2834
2857
  const total = results.length;
2835
2858
  if (options?.limit) {
@@ -2838,6 +2861,15 @@ function createMockAIConversationsRepository(stores) {
2838
2861
  return Promise.resolve({ messages: results, total });
2839
2862
  },
2840
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
+ }
2841
2873
  const results = Array.from(stores.aiMessages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(-count);
2842
2874
  return Promise.resolve(results);
2843
2875
  }
@@ -3104,7 +3136,12 @@ function createMockFilesRepository(stores) {
3104
3136
  function createMockObjectsRepository(stores) {
3105
3137
  return {
3106
3138
  findById(id) {
3107
- 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);
3108
3145
  },
3109
3146
  findByName(name) {
3110
3147
  const tenantId = getTenantId();
@@ -3144,8 +3181,9 @@ function createMockObjectsRepository(stores) {
3144
3181
  return Promise.resolve(obj);
3145
3182
  },
3146
3183
  update(id, data) {
3184
+ const tenantId = getTenantId();
3147
3185
  const existing = stores.objects.get(id);
3148
- if (!existing) {
3186
+ if (!existing || existing.tenantId !== tenantId) {
3149
3187
  return Promise.reject(new Error(`Object ${id} not found`));
3150
3188
  }
3151
3189
  const updated = {
@@ -3157,6 +3195,11 @@ function createMockObjectsRepository(stores) {
3157
3195
  return Promise.resolve(updated);
3158
3196
  },
3159
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
+ }
3160
3203
  stores.objects.delete(id);
3161
3204
  return Promise.resolve();
3162
3205
  },
@@ -3170,7 +3213,7 @@ function createMockObjectsRepository(stores) {
3170
3213
  upsert(data) {
3171
3214
  const tenantId = getTenantId();
3172
3215
  for (const obj of stores.objects.values()) {
3173
- if (obj.system && obj.name === data.name) {
3216
+ if (obj.tenantId === tenantId && obj.system && obj.name === data.name) {
3174
3217
  const updated = {
3175
3218
  ...obj,
3176
3219
  label: data.label,
@@ -3842,6 +3885,7 @@ function createMockObjectRecordsRepository(stores) {
3842
3885
  findById(id) {
3843
3886
  const internal = stores.objectRecords.get(id);
3844
3887
  if (!internal) return Promise.resolve(null);
3888
+ if (internal.deletedAt) return Promise.resolve(null);
3845
3889
  const { tenantId: _, ...record } = internal;
3846
3890
  return Promise.resolve(record);
3847
3891
  },
@@ -3849,7 +3893,7 @@ function createMockObjectRecordsRepository(stores) {
3849
3893
  const results = [];
3850
3894
  for (const id of ids) {
3851
3895
  const internal = stores.objectRecords.get(id);
3852
- if (internal) {
3896
+ if (internal && !internal.deletedAt) {
3853
3897
  const { tenantId: _, ...record } = internal;
3854
3898
  results.push(record);
3855
3899
  }
@@ -3910,13 +3954,18 @@ function createMockObjectRecordsRepository(stores) {
3910
3954
  return Promise.resolve(record);
3911
3955
  },
3912
3956
  delete(id) {
3913
- 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);
3914
3963
  return Promise.resolve();
3915
3964
  },
3916
3965
  list(objectId, options) {
3917
3966
  const tenantId = getTenantId();
3918
3967
  let results = Array.from(stores.objectRecords.values()).filter(
3919
- (r) => r.tenantId === tenantId && r.objectId === objectId
3968
+ (r) => r.tenantId === tenantId && r.objectId === objectId && !r.deletedAt
3920
3969
  );
3921
3970
  const total = results.length;
3922
3971
  if (options?.limit) {
@@ -3929,7 +3978,7 @@ function createMockObjectRecordsRepository(stores) {
3929
3978
  const tenantId = getTenantId();
3930
3979
  const lowerQuery = query.toLowerCase();
3931
3980
  let results = Array.from(stores.objectRecords.values()).filter((r) => {
3932
- if (r.tenantId !== tenantId || r.objectId !== objectId) return false;
3981
+ if (r.tenantId !== tenantId || r.objectId !== objectId || r.deletedAt) return false;
3933
3982
  return Object.values(r.values).some(
3934
3983
  (val) => String(val).toLowerCase().includes(lowerQuery)
3935
3984
  );
@@ -3963,7 +4012,7 @@ function createMockObjectRecordsRepository(stores) {
3963
4012
  }
3964
4013
  }
3965
4014
  let matchingRecords = Array.from(stores.objectRecords.values()).filter((r) => {
3966
- if (r.tenantId !== tenantId) return false;
4015
+ if (r.tenantId !== tenantId || r.deletedAt) return false;
3967
4016
  if (!allowedObjectIds.has(r.objectId)) return false;
3968
4017
  return Object.values(r.values).some(
3969
4018
  (val) => String(val).toLowerCase().includes(lowerQuery)
@@ -4118,12 +4167,12 @@ function createMockObjectRecordsRepository(stores) {
4118
4167
  }
4119
4168
  return Promise.resolve(updated);
4120
4169
  },
4121
- findByRelation(objectName, relationAttributeName, targetRecordId) {
4122
- const obj = Array.from(stores.objects.values()).find((o) => o.name === objectName);
4170
+ findByRelation(objectId, relationAttributeName, targetRecordId) {
4171
+ const obj = stores.objects.get(objectId);
4123
4172
  if (!obj) return Promise.resolve([]);
4124
4173
  const results = [];
4125
4174
  for (const record of stores.objectRecords.values()) {
4126
- if (record.objectId !== obj.id) continue;
4175
+ if (record.objectId !== objectId) continue;
4127
4176
  const value = record.values[relationAttributeName];
4128
4177
  let matches = false;
4129
4178
  if (typeof value === "string" && value === targetRecordId) {
@@ -4283,7 +4332,12 @@ function createEmptyStores() {
4283
4332
  function createMockUserProfilesRepository(stores) {
4284
4333
  return {
4285
4334
  findById(id) {
4286
- 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);
4287
4341
  },
4288
4342
  findByIds(ids) {
4289
4343
  const tenantId = getTenantId();
@@ -4297,8 +4351,9 @@ function createMockUserProfilesRepository(stores) {
4297
4351
  return Promise.resolve(results);
4298
4352
  },
4299
4353
  findByAuthId(authId) {
4354
+ const tenantId = getTenantId();
4300
4355
  for (const profile of stores.userProfiles.values()) {
4301
- if (profile.authId === authId) {
4356
+ if (profile.tenantId === tenantId && profile.authId === authId) {
4302
4357
  return Promise.resolve(profile);
4303
4358
  }
4304
4359
  }
@@ -4886,6 +4941,10 @@ function createMockWorkflowsRepository(stores) {
4886
4941
  if (!existing) {
4887
4942
  return Promise.reject(new Error(`Workflow ${id} not found`));
4888
4943
  }
4944
+ const tenantId = getTenantId();
4945
+ if (existing.tenant_id !== tenantId && !existing.system) {
4946
+ return Promise.reject(new Error(`Workflow ${id} not found`));
4947
+ }
4889
4948
  const updated = {
4890
4949
  ...existing,
4891
4950
  label: data.label ?? existing.label,
@@ -4906,6 +4965,13 @@ function createMockWorkflowsRepository(stores) {
4906
4965
  return Promise.resolve(updated);
4907
4966
  },
4908
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
+ }
4909
4975
  stores.workflows.delete(id);
4910
4976
  return Promise.resolve();
4911
4977
  },
@@ -4988,6 +5054,10 @@ function createMockWorkflowInstancesRepository(stores) {
4988
5054
  if (!existing) {
4989
5055
  return Promise.reject(new Error(`WorkflowInstance ${id} not found`));
4990
5056
  }
5057
+ const tenantId = getTenantId();
5058
+ if (existing.tenant_id !== tenantId) {
5059
+ return Promise.reject(new Error(`WorkflowInstance ${id} not found`));
5060
+ }
4991
5061
  const updated = {
4992
5062
  ...existing,
4993
5063
  status: data.status ?? existing.status,
@@ -5112,6 +5182,10 @@ function createMockWorkflowInvitationsRepository(stores) {
5112
5182
  if (!existing) {
5113
5183
  return Promise.reject(new Error(`WorkflowInvitation ${id} not found`));
5114
5184
  }
5185
+ const tenantId = getTenantId();
5186
+ if (existing.tenant_id !== tenantId) {
5187
+ return Promise.reject(new Error(`WorkflowInvitation ${id} not found`));
5188
+ }
5115
5189
  const updated = {
5116
5190
  ...existing,
5117
5191
  status: data.status ?? existing.status,
@@ -5177,6 +5251,10 @@ function createMockWorkflowAccessGrantsRepository(stores) {
5177
5251
  if (!existing) {
5178
5252
  return Promise.reject(new Error(`WorkflowAccessGrant ${id} not found`));
5179
5253
  }
5254
+ const tenantId = getTenantId();
5255
+ if (existing.tenant_id !== tenantId) {
5256
+ return Promise.reject(new Error(`WorkflowAccessGrant ${id} not found`));
5257
+ }
5180
5258
  const updated = {
5181
5259
  ...existing,
5182
5260
  last_used_at: data.lastUsedAt !== void 0 ? data.lastUsedAt.toISOString() : existing.last_used_at,
@@ -5310,6 +5388,20 @@ var PolicyRegistry = class {
5310
5388
  };
5311
5389
  var defaultPolicyRegistry = new PolicyRegistry();
5312
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
+
5313
5405
  // src/runtime/services/base.service.ts
5314
5406
  var BaseService = class {
5315
5407
  constructor(adapter) {
@@ -5529,6 +5621,15 @@ function isBilateralRelation(attr) {
5529
5621
  function inferInverseCardinality(cardinality) {
5530
5622
  return cardinality === "one" ? "many" : "many";
5531
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
+ }
5532
5633
 
5533
5634
  // src/types/relation-properties.ts
5534
5635
  var FORBIDDEN_PROPERTY_TYPES = [
@@ -6248,6 +6349,20 @@ var SingleRelationAttributeBuilder = class extends BaseRelationAttributeBuilder
6248
6349
  isRequired: this.attr.required
6249
6350
  }
6250
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
+ }
6251
6366
  return multiBuilder;
6252
6367
  }
6253
6368
  required() {
@@ -6488,6 +6603,13 @@ var DocumentAttributeBuilder = class extends BaseAttributeBuilder {
6488
6603
  this.setRequired(true);
6489
6604
  return this;
6490
6605
  }
6606
+ /**
6607
+ * Mark this attribute as optional (undo required).
6608
+ */
6609
+ optional() {
6610
+ this.setRequired(false);
6611
+ return this;
6612
+ }
6491
6613
  };
6492
6614
  function document(config) {
6493
6615
  return new DocumentAttributeBuilder(config.name, config.label);
@@ -8940,6 +9062,19 @@ var ObjectSchemaService = class extends BaseService {
8940
9062
  });
8941
9063
  });
8942
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
+ }
8943
9078
  }
8944
9079
  return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
8945
9080
  }
@@ -11002,10 +11137,29 @@ var RecordQueryService = class extends BaseService {
11002
11137
  if (this.options?.permissionService && this.userId) {
11003
11138
  await checkPermission(this.options.permissionService, this.userId, schema.name, "read");
11004
11139
  }
11005
- const result = await runWithSchemaContext(
11006
- [schema],
11007
- () => this.adapter.objectRecords.search(objectId, query, options)
11008
- );
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
+ }
11009
11163
  const enrichedRecords = await this.relationPropertiesService.enrichRecordsBatch(
11010
11164
  result.records,
11011
11165
  schema
@@ -11021,6 +11175,32 @@ var RecordQueryService = class extends BaseService {
11021
11175
  total: result.total
11022
11176
  };
11023
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
+ }
11024
11204
  };
11025
11205
 
11026
11206
  // src/runtime/services/record/record-resolver.service.ts
@@ -11841,7 +12021,7 @@ var RollupService = class extends BaseService {
11841
12021
  if (typeof relatedId === "string" && relatedId.length > 0) {
11842
12022
  affectedIds.push(relatedId);
11843
12023
  } else if (Array.isArray(relatedId)) {
11844
- affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
12024
+ affectedIds.push(...relatedId.filter((id) => typeof id === "string" && id.length > 0));
11845
12025
  }
11846
12026
  }
11847
12027
  return [...new Set(affectedIds)];
@@ -12073,6 +12253,11 @@ var RecordService = class extends BaseService {
12073
12253
  }
12074
12254
  await recalculateParentRollups(record, schema, this.rollupContext);
12075
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));
12076
12261
  if (this.auditService && this.userId) {
12077
12262
  this.auditService.logRecordAction({
12078
12263
  action: "record.created",
@@ -12236,6 +12421,11 @@ var RecordService = class extends BaseService {
12236
12421
  }
12237
12422
  const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
12238
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));
12239
12429
  for (const [attrName, value] of Object.entries(dataToUpdate)) {
12240
12430
  const attr = schema.attributes.find((a) => a.name === attrName);
12241
12431
  if (attr?.type === "relation") {
@@ -12351,6 +12541,7 @@ var RecordService = class extends BaseService {
12351
12541
  }
12352
12542
  await this.adapter.objectRecords.delete(recordId);
12353
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));
12354
12545
  if (!options?.skipHooks) {
12355
12546
  await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
12356
12547
  }
@@ -12430,6 +12621,11 @@ var RecordService = class extends BaseService {
12430
12621
  }
12431
12622
  const restored = await this.adapter.objectRecords.restore(recordId);
12432
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));
12433
12629
  if (!options?.skipHooks) {
12434
12630
  const afterCtx = {
12435
12631
  ...hookCtx,
@@ -16999,16 +17195,24 @@ var GlobalSearchService = class extends BaseService {
16999
17195
  if (!query || query.trim().length === 0) {
17000
17196
  return { results: [], total: 0 };
17001
17197
  }
17002
- return this.cachedList(
17003
- "globalSearch",
17004
- "search",
17005
- { query: query.trim(), ...options },
17006
- () => this.adapter.objectRecords.globalSearch(query.trim(), {
17007
- limit: options?.limit ?? 20,
17008
- offset: options?.offset ?? 0,
17009
- objectNames: options?.objectNames
17010
- })
17011
- );
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
+ });
17012
17216
  }
17013
17217
  /**
17014
17218
  * Search and group results by object type.
@@ -17022,15 +17226,72 @@ var GlobalSearchService = class extends BaseService {
17022
17226
  if (!query || query.trim().length === 0) {
17023
17227
  return { groups: [], total: 0 };
17024
17228
  }
17025
- return this.cachedList(
17026
- "globalSearch",
17027
- "grouped",
17028
- { query: query.trim(), ...options },
17029
- () => this.adapter.objectRecords.globalSearchGrouped(query.trim(), {
17030
- objectNames: options?.objectNames,
17031
- limitPerGroup: options?.limitPerGroup ?? 5
17032
- })
17033
- );
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
+ };
17034
17295
  }
17035
17296
  };
17036
17297
 
@@ -18218,7 +18479,7 @@ async function syncSingleObject(adapter, nativeObject, result, options) {
18218
18479
  `Cannot sync shared object "${nativeObject.name}": masterTenantId must be configured in tenant options`
18219
18480
  );
18220
18481
  }
18221
- if (options.tenantId && options.tenantId !== options.masterTenantId) {
18482
+ if (!options.tenantId || options.tenantId !== options.masterTenantId) {
18222
18483
  throw new Error(
18223
18484
  `Cannot sync shared object "${nativeObject.name}": only master tenant "${options.masterTenantId}" can sync shared objects (current: "${options.tenantId}")`
18224
18485
  );
@@ -18226,12 +18487,13 @@ async function syncSingleObject(adapter, nativeObject, result, options) {
18226
18487
  }
18227
18488
  const existingObject = await adapter.objects.findSystemByName(nativeObject.name);
18228
18489
  const isNew = !existingObject;
18229
- updateObjectStats(result, isNew);
18230
18490
  if (options.dryRun) {
18491
+ updateObjectStats(result, isNew);
18231
18492
  await handleDryRun(adapter, nativeObject, existingObject, result, options, isNew);
18232
18493
  return;
18233
18494
  }
18234
18495
  const dbObject = await upsertObject(adapter, nativeObject, options);
18496
+ updateObjectStats(result, isNew);
18235
18497
  await syncAttributes(adapter, nativeObject, dbObject, existingObject, result);
18236
18498
  if (!isNew) {
18237
18499
  await cleanupAttributes(adapter, dbObject.id, nativeObject, result);
@@ -18260,6 +18522,11 @@ async function handleDryRun(adapter, nativeObject, existingObject, result, optio
18260
18522
  }
18261
18523
  result.attributesSynced++;
18262
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;
18263
18530
  }
18264
18531
  async function upsertObject(adapter, nativeObject, _options) {
18265
18532
  return await adapter.objects.upsert({
@@ -18374,6 +18641,8 @@ export {
18374
18641
  isUniversalRelation,
18375
18642
  isBilateralRelation,
18376
18643
  inferInverseCardinality,
18644
+ NON_SORTABLE_TYPES,
18645
+ isAttributeSortable,
18377
18646
  RecordReferencedError,
18378
18647
  AttributeInUseError,
18379
18648
  ObjectReferencedError,
@@ -18625,6 +18894,7 @@ export {
18625
18894
  createMockAdapter,
18626
18895
  PolicyRegistry,
18627
18896
  defaultPolicyRegistry,
18897
+ SORTABLE_ATTRIBUTE_TYPES,
18628
18898
  BaseService,
18629
18899
  BaseRepository,
18630
18900
  SchemaContextAwareRepository,