@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.
- package/dist/chunk-6P3NPTYV.mjs +58 -0
- package/dist/chunk-JZO52C3F.js +58 -0
- package/dist/{chunk-INXFKM4S.mjs → chunk-KQMGNSDK.mjs} +413 -111
- package/dist/{chunk-4GHZ6AUF.js → chunk-WXEFNFHT.js} +609 -307
- package/dist/default-roles-76HUWY6T.js +23 -0
- package/dist/{default-roles-42X3TJI5.mjs → default-roles-OAMHLOCS.mjs} +3 -1
- package/dist/index.d.mts +28 -29
- package/dist/index.d.ts +28 -29
- package/dist/index.js +72 -18
- package/dist/index.mjs +61 -7
- package/dist/{runtime-BV9XBP1p.d.ts → runtime-BOHwdAGE.d.ts} +129 -109
- package/dist/{runtime-DzpQ5gRG.d.mts → runtime-CFoct1Bt.d.mts} +129 -109
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +5 -2
- package/dist/runtime.mjs +4 -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
- package/dist/chunk-36UBIXJN.js +0 -82
- package/dist/chunk-V5QXU2OK.mjs +0 -82
- package/dist/default-roles-C3FYDYMN.js +0 -21
|
@@ -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
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
|
2539
|
+
const result = {};
|
|
2527
2540
|
for (const [relationName, values] of Object.entries(resolvedRelations)) {
|
|
2528
|
-
|
|
2529
|
-
flat[`${relationName}.${key}`] = value;
|
|
2530
|
-
}
|
|
2541
|
+
result[relationName] = { ...values };
|
|
2531
2542
|
}
|
|
2532
|
-
return
|
|
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
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
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
|
-
|
|
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
|
|
3704
|
-
const
|
|
3705
|
-
|
|
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 &&
|
|
3708
|
-
for (
|
|
3709
|
-
const { name: pipeName, args } = parsePipeExpression(
|
|
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*([
|
|
3788
|
+
const regex = /\{\{\s*([^}]+)\s*\}\}/g;
|
|
3732
3789
|
let match;
|
|
3733
3790
|
while ((match = regex.exec(template)) !== null) {
|
|
3734
|
-
const
|
|
3735
|
-
const
|
|
3736
|
-
|
|
3737
|
-
|
|
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.
|
|
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(
|
|
4102
|
-
const obj =
|
|
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 !==
|
|
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
|
-
|
|
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
|
-
|
|
4413
|
+
getUsersWithRoles(filters) {
|
|
4340
4414
|
const tenantId = getTenantId();
|
|
4341
|
-
const
|
|
4342
|
-
(
|
|
4343
|
-
)
|
|
4344
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
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
|
-
|
|
15010
|
-
|
|
15011
|
-
|
|
15012
|
-
|
|
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
|
-
|
|
16971
|
-
|
|
16972
|
-
|
|
16973
|
-
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
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
|
-
|
|
16994
|
-
|
|
16995
|
-
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
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-
|
|
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
|
|
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,
|