@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.
- package/dist/{chunk-ABOLTRIR.mjs → chunk-KQMGNSDK.mjs} +326 -56
- package/dist/{chunk-EWDCS5EE.js → chunk-WXEFNFHT.js} +462 -192
- package/dist/index.d.mts +12 -6
- package/dist/index.d.ts +12 -6
- package/dist/index.js +46 -17
- package/dist/index.mjs +35 -6
- package/dist/{runtime-CJVwSgr6.d.ts → runtime-BOHwdAGE.d.ts} +75 -6
- package/dist/{runtime-Ca41jw8q.d.mts → runtime-CFoct1Bt.d.mts} +75 -6
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +4 -2
- package/dist/runtime.mjs +3 -1
- package/dist/validation/validators.d.mts +1 -1
- package/dist/validation/validators.d.ts +1 -1
- package/dist/{validators-DEfgr14O.d.ts → validators-BxPuQ2GT.d.ts} +12 -1
- package/dist/{validators-CzHCpxVj.d.mts → validators-DUB0tEzp.d.mts} +12 -1
- package/package.json +2 -2
|
@@ -2141,7 +2141,7 @@ var EndExecutor = class {
|
|
|
2141
2141
|
this.nodeType = "end";
|
|
2142
2142
|
}
|
|
2143
2143
|
execute(node, _context) {
|
|
2144
|
-
return complete(node.status
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
|
2539
|
+
const result = {};
|
|
2530
2540
|
for (const [relationName, values] of Object.entries(resolvedRelations)) {
|
|
2531
|
-
|
|
2532
|
-
flat[`${relationName}.${key}`] = value;
|
|
2533
|
-
}
|
|
2541
|
+
result[relationName] = { ...values };
|
|
2534
2542
|
}
|
|
2535
|
-
return
|
|
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
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
4122
|
-
const obj =
|
|
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 !==
|
|
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
|
-
|
|
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
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
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
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
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
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
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
|
|
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,
|