@stndrds/schema 0.1.0-alpha.14
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-3ABIOLKO.mjs +4624 -0
- package/dist/chunk-3IRX2BDY.mjs +3970 -0
- package/dist/chunk-4OTLPWSP.mjs +4448 -0
- package/dist/chunk-54ODQBUN.mjs +3867 -0
- package/dist/chunk-6HIA3FR2.mjs +4230 -0
- package/dist/chunk-6P3XHNLD.mjs +4171 -0
- package/dist/chunk-7LJECNAS.mjs +4167 -0
- package/dist/chunk-A2WNLUKV.mjs +4475 -0
- package/dist/chunk-AA3234RM.mjs +4475 -0
- package/dist/chunk-ADNXDLLD.mjs +3136 -0
- package/dist/chunk-B4DCDQUI.mjs +4695 -0
- package/dist/chunk-B4PSXY6I.mjs +4527 -0
- package/dist/chunk-BJHJBZCM.mjs +4228 -0
- package/dist/chunk-C3ZWTO6G.mjs +4474 -0
- package/dist/chunk-CLOGNKDD.mjs +4040 -0
- package/dist/chunk-EW4V63PA.mjs +3053 -0
- package/dist/chunk-FR2LCO6R.mjs +2485 -0
- package/dist/chunk-GJSF2RKL.mjs +2786 -0
- package/dist/chunk-GO5NPP2X.mjs +4726 -0
- package/dist/chunk-H3ZYJUDS.mjs +3050 -0
- package/dist/chunk-IDDLSFSK.mjs +4230 -0
- package/dist/chunk-JRADUJQ7.mjs +4726 -0
- package/dist/chunk-NEDR6N7R.mjs +3118 -0
- package/dist/chunk-NESPRMDF.mjs +2482 -0
- package/dist/chunk-NWFGRJBW.mjs +4612 -0
- package/dist/chunk-NXJ57GSN.mjs +2918 -0
- package/dist/chunk-O4C5FCTS.mjs +3961 -0
- package/dist/chunk-O4PRB27Q.mjs +2513 -0
- package/dist/chunk-OJDFNUCR.mjs +4463 -0
- package/dist/chunk-OSEKVBHC.mjs +4627 -0
- package/dist/chunk-QX4U6U6K.mjs +2484 -0
- package/dist/chunk-RMDQ6LKV.mjs +3046 -0
- package/dist/chunk-SBXKDATH.mjs +2673 -0
- package/dist/chunk-SKMP3AP3.mjs +2918 -0
- package/dist/chunk-STMS7WZD.mjs +2623 -0
- package/dist/chunk-T3T6PTFG.mjs +3057 -0
- package/dist/chunk-TPW72RAH.mjs +4624 -0
- package/dist/chunk-TUQEGNGV.mjs +2534 -0
- package/dist/chunk-TVKFS3YN.mjs +2449 -0
- package/dist/chunk-TZTH3BHG.mjs +3110 -0
- package/dist/chunk-W6MU3XPG.mjs +4612 -0
- package/dist/chunk-WL7Z3YFR.mjs +4603 -0
- package/dist/chunk-XNVVYHH3.mjs +4602 -0
- package/dist/chunk-XTA3WY64.mjs +2918 -0
- package/dist/chunk-YV7DEZFZ.mjs +2528 -0
- package/dist/chunk-ZLJPJND6.mjs +4474 -0
- package/dist/chunk-ZNZXTCEX.mjs +4234 -0
- package/dist/index.d.mts +1222 -0
- package/dist/index.d.ts +1222 -0
- package/dist/index.js +5320 -0
- package/dist/index.mjs +635 -0
- package/dist/runtime-2iKarXtl.d.mts +1924 -0
- package/dist/runtime-2iKarXtl.d.ts +1924 -0
- package/dist/runtime-4l2ddHZ7.d.mts +2242 -0
- package/dist/runtime-4l2ddHZ7.d.ts +2243 -0
- package/dist/runtime-8hKS6zAv.d.mts +2226 -0
- package/dist/runtime-8hKS6zAv.d.ts +2226 -0
- package/dist/runtime-B6yjtoR3.d.mts +4142 -0
- package/dist/runtime-B6yjtoR3.d.ts +4142 -0
- package/dist/runtime-BGCShrZB.d.mts +2001 -0
- package/dist/runtime-BGCShrZB.d.ts +2001 -0
- package/dist/runtime-BRW4NGAk.d.mts +1668 -0
- package/dist/runtime-BRW4NGAk.d.ts +1668 -0
- package/dist/runtime-BnfBr314.d.mts +1683 -0
- package/dist/runtime-BnfBr314.d.ts +1683 -0
- package/dist/runtime-BrqhiJUF.d.mts +4151 -0
- package/dist/runtime-BrqhiJUF.d.ts +4151 -0
- package/dist/runtime-Bvj8c-BE.d.mts +4277 -0
- package/dist/runtime-Bvj8c-BE.d.ts +4277 -0
- package/dist/runtime-C03qT4qU.d.mts +4267 -0
- package/dist/runtime-C03qT4qU.d.ts +4267 -0
- package/dist/runtime-CaCZ7mSf.d.mts +2974 -0
- package/dist/runtime-CaCZ7mSf.d.ts +2974 -0
- package/dist/runtime-CaIzNX0Z.d.mts +4091 -0
- package/dist/runtime-CaIzNX0Z.d.ts +4091 -0
- package/dist/runtime-ChPTgmLP.d.mts +4320 -0
- package/dist/runtime-ChPTgmLP.d.ts +4320 -0
- package/dist/runtime-CnImZ1Vv.d.mts +4267 -0
- package/dist/runtime-CnImZ1Vv.d.ts +4267 -0
- package/dist/runtime-Cp-B26Nj.d.mts +2890 -0
- package/dist/runtime-Cp-B26Nj.d.ts +2890 -0
- package/dist/runtime-Cq8jfk3c.d.mts +4277 -0
- package/dist/runtime-Cq8jfk3c.d.ts +4277 -0
- package/dist/runtime-CqDFXhhP.d.mts +1685 -0
- package/dist/runtime-CqDFXhhP.d.ts +1685 -0
- package/dist/runtime-CqtpZLdL.d.mts +4280 -0
- package/dist/runtime-CqtpZLdL.d.ts +4280 -0
- package/dist/runtime-D-4DblaZ.d.mts +4147 -0
- package/dist/runtime-D-4DblaZ.d.ts +4147 -0
- package/dist/runtime-D59sSqNl.d.mts +4146 -0
- package/dist/runtime-D59sSqNl.d.ts +4146 -0
- package/dist/runtime-DEfPk3wT.d.mts +1840 -0
- package/dist/runtime-DEfPk3wT.d.ts +1840 -0
- package/dist/runtime-DGV-7vES.d.mts +1685 -0
- package/dist/runtime-DGV-7vES.d.ts +1685 -0
- package/dist/runtime-DNacghqc.d.mts +3971 -0
- package/dist/runtime-DNacghqc.d.ts +3971 -0
- package/dist/runtime-DPLsUHYK.d.mts +3977 -0
- package/dist/runtime-DPLsUHYK.d.ts +3977 -0
- package/dist/runtime-DTxCw60F.d.mts +2973 -0
- package/dist/runtime-DTxCw60F.d.ts +2973 -0
- package/dist/runtime-DUXJ-zNS.d.mts +2788 -0
- package/dist/runtime-DUXJ-zNS.d.ts +2788 -0
- package/dist/runtime-DbljInhV.d.mts +2237 -0
- package/dist/runtime-DbljInhV.d.ts +2237 -0
- package/dist/runtime-DcH3y6Yu.d.mts +4272 -0
- package/dist/runtime-DcH3y6Yu.d.ts +4272 -0
- package/dist/runtime-DdBFz85g.d.mts +4229 -0
- package/dist/runtime-DdBFz85g.d.ts +4229 -0
- package/dist/runtime-DjMWhjkm.d.mts +4321 -0
- package/dist/runtime-DjMWhjkm.d.ts +4321 -0
- package/dist/runtime-Djz3fOBE.d.mts +2078 -0
- package/dist/runtime-Djz3fOBE.d.ts +2078 -0
- package/dist/runtime-Dli8g3Aq.d.mts +2082 -0
- package/dist/runtime-Dli8g3Aq.d.ts +2082 -0
- package/dist/runtime-a8J378im.d.mts +4123 -0
- package/dist/runtime-a8J378im.d.ts +4123 -0
- package/dist/runtime-dp3rb3Lf.d.mts +2968 -0
- package/dist/runtime-dp3rb3Lf.d.ts +2968 -0
- package/dist/runtime-iWXlSN_H.d.mts +1683 -0
- package/dist/runtime-iWXlSN_H.d.ts +1683 -0
- package/dist/runtime-yMql0npY.d.mts +4321 -0
- package/dist/runtime-yMql0npY.d.ts +4321 -0
- package/dist/runtime.d.mts +3 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +3626 -0
- package/dist/runtime.mjs +48 -0
- package/package.json +58 -0
|
@@ -0,0 +1,4612 @@
|
|
|
1
|
+
// src/runtime/hooks/hook.registry.ts
|
|
2
|
+
var NoopHookRegistry = class {
|
|
3
|
+
async execute() {
|
|
4
|
+
}
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/utils.ts
|
|
8
|
+
function generateId() {
|
|
9
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
10
|
+
return crypto.randomUUID();
|
|
11
|
+
}
|
|
12
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
13
|
+
const r = Math.random() * 16 | 0;
|
|
14
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
15
|
+
return v.toString(16);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function generatePrefixedId(prefix) {
|
|
19
|
+
return `${prefix}_${generateId()}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/runtime/template.ts
|
|
23
|
+
var pipes = {
|
|
24
|
+
/** Convert to uppercase */
|
|
25
|
+
UPPER: (v) => String(v).toUpperCase(),
|
|
26
|
+
/** Convert to lowercase */
|
|
27
|
+
LOWER: (v) => String(v).toLowerCase(),
|
|
28
|
+
/** Capitalize first letter of each word */
|
|
29
|
+
capitalize: (v) => String(v).replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
30
|
+
/** Trim whitespace from both ends */
|
|
31
|
+
trim: (v) => String(v).trim()
|
|
32
|
+
};
|
|
33
|
+
function getValue(obj, path) {
|
|
34
|
+
return path.split(".").reduce((acc, key) => {
|
|
35
|
+
if (acc == null || typeof acc !== "object") return void 0;
|
|
36
|
+
return acc[key];
|
|
37
|
+
}, obj);
|
|
38
|
+
}
|
|
39
|
+
var DEFAULT_LABEL_FALLBACK = "(Untitled)";
|
|
40
|
+
function renderLabelExpression(template, values, fallback = DEFAULT_LABEL_FALLBACK) {
|
|
41
|
+
const result = template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_, expr) => {
|
|
42
|
+
const parts = expr.split("|").map((s) => s.trim());
|
|
43
|
+
const path = parts[0];
|
|
44
|
+
let value = getValue(values, path);
|
|
45
|
+
if (value == null || value === "") return "";
|
|
46
|
+
for (let i = 1; i < parts.length; i++) {
|
|
47
|
+
const pipeName = parts[i].trim();
|
|
48
|
+
const fn = pipes[pipeName];
|
|
49
|
+
if (fn) {
|
|
50
|
+
value = fn(String(value));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return String(value);
|
|
54
|
+
}).trim();
|
|
55
|
+
return result || fallback;
|
|
56
|
+
}
|
|
57
|
+
function isLabelExpression(value) {
|
|
58
|
+
return /\{\{\s*\S+.*\}\}/.test(value);
|
|
59
|
+
}
|
|
60
|
+
function extractAttributeNames(template) {
|
|
61
|
+
const names = [];
|
|
62
|
+
const regex = /\{\{\s*([^|}]+)/g;
|
|
63
|
+
let match;
|
|
64
|
+
while ((match = regex.exec(template)) !== null) {
|
|
65
|
+
const path = match[1].trim();
|
|
66
|
+
const rootName = path.split(".")[0];
|
|
67
|
+
if (rootName && !names.includes(rootName)) {
|
|
68
|
+
names.push(rootName);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return names;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/runtime/mock-adapter.ts
|
|
75
|
+
function createMockObjectsRepository(stores) {
|
|
76
|
+
return {
|
|
77
|
+
findById(id) {
|
|
78
|
+
return Promise.resolve(stores.objects.get(id) ?? null);
|
|
79
|
+
},
|
|
80
|
+
findByName(tenantId, name) {
|
|
81
|
+
for (const obj of stores.objects.values()) {
|
|
82
|
+
if (obj.tenantId === tenantId && obj.name === name) {
|
|
83
|
+
return Promise.resolve(obj);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return Promise.resolve(null);
|
|
87
|
+
},
|
|
88
|
+
findSystemByName(name) {
|
|
89
|
+
for (const obj of stores.objects.values()) {
|
|
90
|
+
if (obj.system && obj.name === name) {
|
|
91
|
+
return Promise.resolve(obj);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return Promise.resolve(null);
|
|
95
|
+
},
|
|
96
|
+
create(data) {
|
|
97
|
+
const obj = {
|
|
98
|
+
id: generateId(),
|
|
99
|
+
tenantId: data.tenantId,
|
|
100
|
+
name: data.name,
|
|
101
|
+
label: data.label,
|
|
102
|
+
description: data.description,
|
|
103
|
+
icon: data.icon,
|
|
104
|
+
labelExpression: data.labelExpression,
|
|
105
|
+
system: data.system ?? false,
|
|
106
|
+
metadata: data.metadata,
|
|
107
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
108
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
109
|
+
};
|
|
110
|
+
stores.objects.set(obj.id, obj);
|
|
111
|
+
return Promise.resolve(obj);
|
|
112
|
+
},
|
|
113
|
+
update(id, data) {
|
|
114
|
+
const existing = stores.objects.get(id);
|
|
115
|
+
if (!existing) {
|
|
116
|
+
return Promise.reject(new Error(`Object ${id} not found`));
|
|
117
|
+
}
|
|
118
|
+
const updated = {
|
|
119
|
+
...existing,
|
|
120
|
+
...data,
|
|
121
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
122
|
+
};
|
|
123
|
+
stores.objects.set(id, updated);
|
|
124
|
+
return Promise.resolve(updated);
|
|
125
|
+
},
|
|
126
|
+
delete(id) {
|
|
127
|
+
stores.objects.delete(id);
|
|
128
|
+
return Promise.resolve();
|
|
129
|
+
},
|
|
130
|
+
list(tenantId) {
|
|
131
|
+
const results = Array.from(stores.objects.values()).filter(
|
|
132
|
+
(obj) => obj.tenantId === tenantId
|
|
133
|
+
);
|
|
134
|
+
return Promise.resolve(results);
|
|
135
|
+
},
|
|
136
|
+
upsert(data) {
|
|
137
|
+
for (const obj of stores.objects.values()) {
|
|
138
|
+
if (obj.system && obj.name === data.name) {
|
|
139
|
+
const updated = {
|
|
140
|
+
...obj,
|
|
141
|
+
label: data.label,
|
|
142
|
+
description: data.description,
|
|
143
|
+
icon: data.icon,
|
|
144
|
+
metadata: data.metadata,
|
|
145
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
146
|
+
};
|
|
147
|
+
stores.objects.set(obj.id, updated);
|
|
148
|
+
return Promise.resolve(updated);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const newObj = {
|
|
152
|
+
id: generateId(),
|
|
153
|
+
tenantId: data.tenantId,
|
|
154
|
+
name: data.name,
|
|
155
|
+
label: data.label,
|
|
156
|
+
description: data.description,
|
|
157
|
+
icon: data.icon,
|
|
158
|
+
labelExpression: data.labelExpression,
|
|
159
|
+
system: data.system,
|
|
160
|
+
metadata: data.metadata,
|
|
161
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
162
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
163
|
+
};
|
|
164
|
+
stores.objects.set(newObj.id, newObj);
|
|
165
|
+
return Promise.resolve(newObj);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function createMockAttributesRepository(stores) {
|
|
170
|
+
return {
|
|
171
|
+
findById(id) {
|
|
172
|
+
return Promise.resolve(stores.attributes.get(id) ?? null);
|
|
173
|
+
},
|
|
174
|
+
findByObjectId(objectId) {
|
|
175
|
+
const results = Array.from(stores.attributes.values()).filter(
|
|
176
|
+
(attr) => attr.objectId === objectId
|
|
177
|
+
);
|
|
178
|
+
return Promise.resolve(results);
|
|
179
|
+
},
|
|
180
|
+
create(data) {
|
|
181
|
+
const attr = {
|
|
182
|
+
id: generateId(),
|
|
183
|
+
objectId: data.objectId,
|
|
184
|
+
name: data.name,
|
|
185
|
+
label: data.label,
|
|
186
|
+
type: data.type,
|
|
187
|
+
description: data.description,
|
|
188
|
+
icon: data.icon,
|
|
189
|
+
system: data.system ?? false,
|
|
190
|
+
required: data.required ?? false,
|
|
191
|
+
unique: data.unique ?? false,
|
|
192
|
+
order: data.order ?? 0,
|
|
193
|
+
hidden: data.hidden,
|
|
194
|
+
archived: data.archived,
|
|
195
|
+
deprecated: data.deprecated,
|
|
196
|
+
defaultValue: data.defaultValue,
|
|
197
|
+
config: data.config ?? {},
|
|
198
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
199
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
200
|
+
};
|
|
201
|
+
stores.attributes.set(attr.id, attr);
|
|
202
|
+
return Promise.resolve(attr);
|
|
203
|
+
},
|
|
204
|
+
update(id, data) {
|
|
205
|
+
const existing = stores.attributes.get(id);
|
|
206
|
+
if (!existing) {
|
|
207
|
+
return Promise.reject(new Error(`Attribute ${id} not found`));
|
|
208
|
+
}
|
|
209
|
+
const updated = {
|
|
210
|
+
...existing,
|
|
211
|
+
...data,
|
|
212
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
213
|
+
};
|
|
214
|
+
stores.attributes.set(id, updated);
|
|
215
|
+
return Promise.resolve(updated);
|
|
216
|
+
},
|
|
217
|
+
delete(id) {
|
|
218
|
+
stores.attributes.delete(id);
|
|
219
|
+
return Promise.resolve();
|
|
220
|
+
},
|
|
221
|
+
deleteByNames(objectId, excludeNames) {
|
|
222
|
+
let count = 0;
|
|
223
|
+
for (const [id, attr] of stores.attributes.entries()) {
|
|
224
|
+
if (attr.objectId === objectId && !excludeNames.includes(attr.name)) {
|
|
225
|
+
stores.attributes.delete(id);
|
|
226
|
+
count++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return Promise.resolve(count);
|
|
230
|
+
},
|
|
231
|
+
upsert(data) {
|
|
232
|
+
for (const attr of stores.attributes.values()) {
|
|
233
|
+
if (attr.objectId === data.objectId && attr.name === data.name) {
|
|
234
|
+
const updated = {
|
|
235
|
+
...attr,
|
|
236
|
+
label: data.label ?? attr.label,
|
|
237
|
+
type: data.type ?? attr.type,
|
|
238
|
+
description: data.description ?? attr.description,
|
|
239
|
+
icon: data.icon ?? attr.icon,
|
|
240
|
+
system: data.system ?? attr.system,
|
|
241
|
+
required: data.required ?? attr.required,
|
|
242
|
+
unique: data.unique ?? attr.unique,
|
|
243
|
+
order: data.order ?? attr.order,
|
|
244
|
+
hidden: data.hidden ?? attr.hidden,
|
|
245
|
+
archived: data.archived ?? attr.archived,
|
|
246
|
+
deprecated: data.deprecated ?? attr.deprecated,
|
|
247
|
+
defaultValue: data.defaultValue ?? attr.defaultValue,
|
|
248
|
+
config: data.config ?? attr.config,
|
|
249
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
250
|
+
};
|
|
251
|
+
stores.attributes.set(attr.id, updated);
|
|
252
|
+
return Promise.resolve(updated);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return this.create(data);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function createMockUserProfilesRepository(stores) {
|
|
260
|
+
return {
|
|
261
|
+
findById(id) {
|
|
262
|
+
return Promise.resolve(stores.userProfiles.get(id) ?? null);
|
|
263
|
+
},
|
|
264
|
+
findByAuthId(authId) {
|
|
265
|
+
for (const profile of stores.userProfiles.values()) {
|
|
266
|
+
if (profile.authId === authId) {
|
|
267
|
+
return Promise.resolve(profile);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return Promise.resolve(null);
|
|
271
|
+
},
|
|
272
|
+
findByEmail(tenantId, email) {
|
|
273
|
+
for (const profile of stores.userProfiles.values()) {
|
|
274
|
+
if (profile.tenantId === tenantId && profile.email === email) {
|
|
275
|
+
return Promise.resolve(profile);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return Promise.resolve(null);
|
|
279
|
+
},
|
|
280
|
+
create(data) {
|
|
281
|
+
const profile = {
|
|
282
|
+
id: generateId(),
|
|
283
|
+
tenantId: data.tenantId,
|
|
284
|
+
authId: data.authId,
|
|
285
|
+
email: data.email,
|
|
286
|
+
firstName: data.firstName,
|
|
287
|
+
lastName: data.lastName,
|
|
288
|
+
avatarUrl: data.avatarUrl,
|
|
289
|
+
role: data.role ?? "member",
|
|
290
|
+
status: data.status ?? "active",
|
|
291
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
292
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
293
|
+
};
|
|
294
|
+
stores.userProfiles.set(profile.id, profile);
|
|
295
|
+
return Promise.resolve(profile);
|
|
296
|
+
},
|
|
297
|
+
update(id, data) {
|
|
298
|
+
const existing = stores.userProfiles.get(id);
|
|
299
|
+
if (!existing) {
|
|
300
|
+
return Promise.reject(new Error(`UserProfile ${id} not found`));
|
|
301
|
+
}
|
|
302
|
+
const updated = {
|
|
303
|
+
...existing,
|
|
304
|
+
...data,
|
|
305
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
306
|
+
};
|
|
307
|
+
stores.userProfiles.set(id, updated);
|
|
308
|
+
return Promise.resolve(updated);
|
|
309
|
+
},
|
|
310
|
+
delete(id) {
|
|
311
|
+
stores.userProfiles.delete(id);
|
|
312
|
+
return Promise.resolve();
|
|
313
|
+
},
|
|
314
|
+
list(tenantId, options) {
|
|
315
|
+
let results = Array.from(stores.userProfiles.values()).filter((p) => p.tenantId === tenantId);
|
|
316
|
+
if (options?.limit) {
|
|
317
|
+
results = results.slice(options.offset ?? 0, (options.offset ?? 0) + options.limit);
|
|
318
|
+
}
|
|
319
|
+
return Promise.resolve(results);
|
|
320
|
+
},
|
|
321
|
+
updateLastLogin(id) {
|
|
322
|
+
const profile = stores.userProfiles.get(id);
|
|
323
|
+
if (profile) {
|
|
324
|
+
profile.lastLoginAt = /* @__PURE__ */ new Date();
|
|
325
|
+
stores.userProfiles.set(id, profile);
|
|
326
|
+
}
|
|
327
|
+
return Promise.resolve();
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function createMockFilesRepository(stores) {
|
|
332
|
+
return {
|
|
333
|
+
findById(id) {
|
|
334
|
+
const file2 = stores.files.get(id);
|
|
335
|
+
if (file2?.deletedAt) return Promise.resolve(null);
|
|
336
|
+
return Promise.resolve(file2 ?? null);
|
|
337
|
+
},
|
|
338
|
+
create(data) {
|
|
339
|
+
const file2 = {
|
|
340
|
+
id: generateId(),
|
|
341
|
+
tenantId: data.tenantId,
|
|
342
|
+
name: data.name,
|
|
343
|
+
originalName: data.originalName,
|
|
344
|
+
mimeType: data.mimeType,
|
|
345
|
+
size: data.size,
|
|
346
|
+
storageProvider: data.storageProvider,
|
|
347
|
+
storagePath: data.storagePath,
|
|
348
|
+
storageBucket: data.storageBucket,
|
|
349
|
+
url: data.url,
|
|
350
|
+
uploadedBy: data.uploadedBy,
|
|
351
|
+
folderPath: data.folderPath,
|
|
352
|
+
tags: data.tags,
|
|
353
|
+
visibility: data.visibility ?? "private",
|
|
354
|
+
allowedUsers: data.allowedUsers,
|
|
355
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
356
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
357
|
+
};
|
|
358
|
+
stores.files.set(file2.id, file2);
|
|
359
|
+
return Promise.resolve(file2);
|
|
360
|
+
},
|
|
361
|
+
update(id, data) {
|
|
362
|
+
const existing = stores.files.get(id);
|
|
363
|
+
if (!existing) {
|
|
364
|
+
return Promise.reject(new Error(`File ${id} not found`));
|
|
365
|
+
}
|
|
366
|
+
const updated = {
|
|
367
|
+
...existing,
|
|
368
|
+
...data,
|
|
369
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
370
|
+
};
|
|
371
|
+
stores.files.set(id, updated);
|
|
372
|
+
return Promise.resolve(updated);
|
|
373
|
+
},
|
|
374
|
+
delete(id) {
|
|
375
|
+
const file2 = stores.files.get(id);
|
|
376
|
+
if (file2) {
|
|
377
|
+
file2.deletedAt = /* @__PURE__ */ new Date();
|
|
378
|
+
stores.files.set(id, file2);
|
|
379
|
+
}
|
|
380
|
+
return Promise.resolve();
|
|
381
|
+
},
|
|
382
|
+
hardDelete(id) {
|
|
383
|
+
stores.files.delete(id);
|
|
384
|
+
return Promise.resolve();
|
|
385
|
+
},
|
|
386
|
+
list(tenantId, options) {
|
|
387
|
+
let results = Array.from(stores.files.values()).filter(
|
|
388
|
+
(f) => f.tenantId === tenantId && !f.deletedAt
|
|
389
|
+
);
|
|
390
|
+
if (options?.mimeType) {
|
|
391
|
+
results = results.filter((f) => f.mimeType === options.mimeType);
|
|
392
|
+
}
|
|
393
|
+
if (options?.limit) {
|
|
394
|
+
results = results.slice(options.offset ?? 0, (options.offset ?? 0) + options.limit);
|
|
395
|
+
}
|
|
396
|
+
return Promise.resolve(results);
|
|
397
|
+
},
|
|
398
|
+
findByFolder(tenantId, folderPath) {
|
|
399
|
+
const results = Array.from(stores.files.values()).filter(
|
|
400
|
+
(f) => f.tenantId === tenantId && f.folderPath === folderPath && !f.deletedAt
|
|
401
|
+
);
|
|
402
|
+
return Promise.resolve(results);
|
|
403
|
+
},
|
|
404
|
+
findByUploader(uploadedBy) {
|
|
405
|
+
const results = Array.from(stores.files.values()).filter(
|
|
406
|
+
(f) => f.uploadedBy === uploadedBy && !f.deletedAt
|
|
407
|
+
);
|
|
408
|
+
return Promise.resolve(results);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function createMockObjectRecordsRepository(stores) {
|
|
413
|
+
return {
|
|
414
|
+
findById(id) {
|
|
415
|
+
const internal = stores.objectRecords.get(id);
|
|
416
|
+
if (!internal) return Promise.resolve(null);
|
|
417
|
+
const { tenantId: _, ...record } = internal;
|
|
418
|
+
return Promise.resolve(record);
|
|
419
|
+
},
|
|
420
|
+
create(data) {
|
|
421
|
+
const internal = {
|
|
422
|
+
id: generateId(),
|
|
423
|
+
tenantId: data.tenantId,
|
|
424
|
+
objectId: data.objectId,
|
|
425
|
+
label: data.label,
|
|
426
|
+
completionStatus: data.completionStatus,
|
|
427
|
+
values: data.data,
|
|
428
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
429
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
430
|
+
};
|
|
431
|
+
stores.objectRecords.set(internal.id, internal);
|
|
432
|
+
const { tenantId: _, ...record } = internal;
|
|
433
|
+
return Promise.resolve(record);
|
|
434
|
+
},
|
|
435
|
+
update(id, data) {
|
|
436
|
+
const existing = stores.objectRecords.get(id);
|
|
437
|
+
if (!existing) {
|
|
438
|
+
return Promise.reject(new Error(`ObjectRecord ${id} not found`));
|
|
439
|
+
}
|
|
440
|
+
const { __completionStatus, __label, ...valueData } = data;
|
|
441
|
+
const updated = {
|
|
442
|
+
...existing,
|
|
443
|
+
label: __label ?? existing.label,
|
|
444
|
+
completionStatus: __completionStatus ?? existing.completionStatus,
|
|
445
|
+
values: { ...existing.values, ...valueData },
|
|
446
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
447
|
+
};
|
|
448
|
+
stores.objectRecords.set(id, updated);
|
|
449
|
+
const { tenantId: _, ...record } = updated;
|
|
450
|
+
return Promise.resolve(record);
|
|
451
|
+
},
|
|
452
|
+
delete(id) {
|
|
453
|
+
stores.objectRecords.delete(id);
|
|
454
|
+
return Promise.resolve();
|
|
455
|
+
},
|
|
456
|
+
list(tenantId, objectId, options) {
|
|
457
|
+
let results = Array.from(stores.objectRecords.values()).filter(
|
|
458
|
+
(r) => r.tenantId === tenantId && r.objectId === objectId
|
|
459
|
+
);
|
|
460
|
+
const total = results.length;
|
|
461
|
+
if (options?.limit) {
|
|
462
|
+
results = results.slice(options.offset ?? 0, (options.offset ?? 0) + options.limit);
|
|
463
|
+
}
|
|
464
|
+
const records = results.map(({ tenantId: _, ...r }) => r);
|
|
465
|
+
return Promise.resolve({ records, total });
|
|
466
|
+
},
|
|
467
|
+
search(tenantId, objectId, query, options) {
|
|
468
|
+
const lowerQuery = query.toLowerCase();
|
|
469
|
+
let results = Array.from(stores.objectRecords.values()).filter((r) => {
|
|
470
|
+
if (r.tenantId !== tenantId || r.objectId !== objectId) return false;
|
|
471
|
+
return Object.values(r.values).some(
|
|
472
|
+
(val) => String(val).toLowerCase().includes(lowerQuery)
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
const total = results.length;
|
|
476
|
+
if (options?.limit) {
|
|
477
|
+
results = results.slice(options.offset ?? 0, (options.offset ?? 0) + options.limit);
|
|
478
|
+
}
|
|
479
|
+
const records = results.map(({ tenantId: _, ...r }) => r);
|
|
480
|
+
return Promise.resolve({ records, total });
|
|
481
|
+
},
|
|
482
|
+
globalSearch(tenantId, query, options) {
|
|
483
|
+
const lowerQuery = query.toLowerCase();
|
|
484
|
+
const objectsMap = /* @__PURE__ */ new Map();
|
|
485
|
+
for (const obj of stores.objects.values()) {
|
|
486
|
+
if (obj.tenantId === tenantId) {
|
|
487
|
+
objectsMap.set(obj.id, obj);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const allowedObjectIds = /* @__PURE__ */ new Set();
|
|
491
|
+
if (options?.objectNames && options.objectNames.length > 0) {
|
|
492
|
+
for (const obj of objectsMap.values()) {
|
|
493
|
+
if (options.objectNames.includes(obj.name)) {
|
|
494
|
+
allowedObjectIds.add(obj.id);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
for (const obj of objectsMap.values()) {
|
|
499
|
+
allowedObjectIds.add(obj.id);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
let matchingRecords = Array.from(stores.objectRecords.values()).filter((r) => {
|
|
503
|
+
if (r.tenantId !== tenantId) return false;
|
|
504
|
+
if (!allowedObjectIds.has(r.objectId)) return false;
|
|
505
|
+
return Object.values(r.values).some(
|
|
506
|
+
(val) => String(val).toLowerCase().includes(lowerQuery)
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
const total = matchingRecords.length;
|
|
510
|
+
if (options?.limit) {
|
|
511
|
+
matchingRecords = matchingRecords.slice(
|
|
512
|
+
options.offset ?? 0,
|
|
513
|
+
(options.offset ?? 0) + options.limit
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const results = matchingRecords.map((r) => {
|
|
517
|
+
const obj = objectsMap.get(r.objectId);
|
|
518
|
+
const labelExpression = obj?.labelExpression ?? "{{ name }}";
|
|
519
|
+
return {
|
|
520
|
+
objectId: r.objectId,
|
|
521
|
+
objectName: obj?.name ?? "unknown",
|
|
522
|
+
objectLabel: obj?.label ?? "Unknown",
|
|
523
|
+
label: renderLabelExpression(labelExpression, r.values),
|
|
524
|
+
recordId: r.id,
|
|
525
|
+
values: r.values,
|
|
526
|
+
completionStatus: r.completionStatus,
|
|
527
|
+
createdAt: r.createdAt,
|
|
528
|
+
updatedAt: r.updatedAt
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
return Promise.resolve({ results, total });
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function createMockViewsRepository(stores) {
|
|
536
|
+
return {
|
|
537
|
+
findById(id) {
|
|
538
|
+
return Promise.resolve(stores.views.get(id) ?? null);
|
|
539
|
+
},
|
|
540
|
+
findByName(tenantId, objectName, viewName) {
|
|
541
|
+
return Promise.resolve(
|
|
542
|
+
Array.from(stores.views.values()).find(
|
|
543
|
+
(v) => v.tenantId === tenantId && v.objectName === objectName && v.name === viewName
|
|
544
|
+
) ?? null
|
|
545
|
+
);
|
|
546
|
+
},
|
|
547
|
+
findByObjectName(tenantId, objectName) {
|
|
548
|
+
return Promise.resolve(
|
|
549
|
+
Array.from(stores.views.values()).filter(
|
|
550
|
+
(v) => v.tenantId === tenantId && v.objectName === objectName
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
},
|
|
554
|
+
findAllForTenant(tenantId) {
|
|
555
|
+
return Promise.resolve(
|
|
556
|
+
Array.from(stores.views.values()).filter((v) => v.tenantId === tenantId)
|
|
557
|
+
);
|
|
558
|
+
},
|
|
559
|
+
findSystemByName(objectName, viewName) {
|
|
560
|
+
return Promise.resolve(
|
|
561
|
+
Array.from(stores.views.values()).find(
|
|
562
|
+
(v) => v.objectName === objectName && v.name === viewName && v.system
|
|
563
|
+
) ?? null
|
|
564
|
+
);
|
|
565
|
+
},
|
|
566
|
+
findSystemByObjectName(objectName) {
|
|
567
|
+
return Promise.resolve(
|
|
568
|
+
Array.from(stores.views.values()).filter((v) => v.objectName === objectName && v.system)
|
|
569
|
+
);
|
|
570
|
+
},
|
|
571
|
+
create(data) {
|
|
572
|
+
const id = generateId();
|
|
573
|
+
const now = /* @__PURE__ */ new Date();
|
|
574
|
+
const dbView = {
|
|
575
|
+
id,
|
|
576
|
+
tenantId: data.tenantId,
|
|
577
|
+
objectName: data.objectName,
|
|
578
|
+
name: data.name,
|
|
579
|
+
label: data.label,
|
|
580
|
+
description: data.description,
|
|
581
|
+
icon: data.icon,
|
|
582
|
+
tabs: data.tabs,
|
|
583
|
+
default: data.default ?? false,
|
|
584
|
+
system: data.system ?? false,
|
|
585
|
+
metadata: data.metadata,
|
|
586
|
+
createdAt: now,
|
|
587
|
+
updatedAt: now
|
|
588
|
+
};
|
|
589
|
+
stores.views.set(id, dbView);
|
|
590
|
+
return Promise.resolve(dbView);
|
|
591
|
+
},
|
|
592
|
+
update(id, data) {
|
|
593
|
+
const view2 = stores.views.get(id);
|
|
594
|
+
if (!view2) {
|
|
595
|
+
return Promise.reject(new Error(`View not found: ${id}`));
|
|
596
|
+
}
|
|
597
|
+
const updated = {
|
|
598
|
+
...view2,
|
|
599
|
+
...data,
|
|
600
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
601
|
+
};
|
|
602
|
+
stores.views.set(id, updated);
|
|
603
|
+
return Promise.resolve(updated);
|
|
604
|
+
},
|
|
605
|
+
delete(id) {
|
|
606
|
+
stores.views.delete(id);
|
|
607
|
+
return Promise.resolve();
|
|
608
|
+
},
|
|
609
|
+
deleteNotIn(objectName, keepViewNames) {
|
|
610
|
+
let deleted = 0;
|
|
611
|
+
for (const [id, view2] of stores.views.entries()) {
|
|
612
|
+
if (view2.objectName === objectName && view2.system && !keepViewNames.includes(view2.name)) {
|
|
613
|
+
stores.views.delete(id);
|
|
614
|
+
deleted++;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return Promise.resolve(deleted);
|
|
618
|
+
},
|
|
619
|
+
async upsert(data) {
|
|
620
|
+
const existing = data.system ? await this.findSystemByName(data.objectName, data.name) : await this.findByName(data.tenantId, data.objectName, data.name);
|
|
621
|
+
if (existing) {
|
|
622
|
+
return this.update(existing.id, {
|
|
623
|
+
label: data.label,
|
|
624
|
+
description: data.description,
|
|
625
|
+
icon: data.icon,
|
|
626
|
+
tabs: data.tabs,
|
|
627
|
+
default: data.default,
|
|
628
|
+
metadata: data.metadata
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return this.create(data);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function createMockAdapter() {
|
|
636
|
+
const stores = {
|
|
637
|
+
objects: /* @__PURE__ */ new Map(),
|
|
638
|
+
attributes: /* @__PURE__ */ new Map(),
|
|
639
|
+
userProfiles: /* @__PURE__ */ new Map(),
|
|
640
|
+
files: /* @__PURE__ */ new Map(),
|
|
641
|
+
objectRecords: /* @__PURE__ */ new Map(),
|
|
642
|
+
views: /* @__PURE__ */ new Map()
|
|
643
|
+
};
|
|
644
|
+
const adapter = {
|
|
645
|
+
objects: createMockObjectsRepository(stores),
|
|
646
|
+
attributes: createMockAttributesRepository(stores),
|
|
647
|
+
userProfiles: createMockUserProfilesRepository(stores),
|
|
648
|
+
files: createMockFilesRepository(stores),
|
|
649
|
+
objectRecords: createMockObjectRecordsRepository(stores),
|
|
650
|
+
views: createMockViewsRepository(stores),
|
|
651
|
+
async transaction(callback) {
|
|
652
|
+
return await callback(adapter);
|
|
653
|
+
},
|
|
654
|
+
// Expose stores for testing inspection
|
|
655
|
+
_stores: stores,
|
|
656
|
+
// Reset all stores (useful between tests)
|
|
657
|
+
reset() {
|
|
658
|
+
stores.objects.clear();
|
|
659
|
+
stores.attributes.clear();
|
|
660
|
+
stores.userProfiles.clear();
|
|
661
|
+
stores.files.clear();
|
|
662
|
+
stores.objectRecords.clear();
|
|
663
|
+
stores.views.clear();
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
return adapter;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/runtime/services/file.service.ts
|
|
670
|
+
var FileService = class {
|
|
671
|
+
constructor(adapter, tenantId) {
|
|
672
|
+
this.adapter = adapter;
|
|
673
|
+
this.tenantId = tenantId;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Create a new file record (after upload to storage)
|
|
677
|
+
*
|
|
678
|
+
* @param data - File metadata
|
|
679
|
+
* @returns Created file record
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* ```typescript
|
|
683
|
+
* const service = new FileService(adapter, "tenant-123");
|
|
684
|
+
*
|
|
685
|
+
* // After uploading to S3
|
|
686
|
+
* const file = await service.createFile({
|
|
687
|
+
* tenantId: "tenant-123",
|
|
688
|
+
* name: "contract-2025.pdf",
|
|
689
|
+
* originalName: "Contract Acme Corp 2025.pdf",
|
|
690
|
+
* mimeType: "application/pdf",
|
|
691
|
+
* size: 2458624,
|
|
692
|
+
* storageProvider: "s3",
|
|
693
|
+
* storagePath: "tenants/123/files/2025/contract.pdf",
|
|
694
|
+
* storageBucket: "my-app-files",
|
|
695
|
+
* url: "https://cdn.example.com/files/file-123",
|
|
696
|
+
* uploadedBy: "profile-456",
|
|
697
|
+
* visibility: "private"
|
|
698
|
+
* });
|
|
699
|
+
* ```
|
|
700
|
+
*/
|
|
701
|
+
async createFile(data) {
|
|
702
|
+
if (data.tenantId !== this.tenantId) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return await this.adapter.files.create(data);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Get file by ID
|
|
711
|
+
*/
|
|
712
|
+
async getFile(fileId) {
|
|
713
|
+
const file2 = await this.adapter.files.findById(fileId);
|
|
714
|
+
if (file2?.deletedAt) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
return file2;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Get file by ID or throw
|
|
721
|
+
*/
|
|
722
|
+
async getFileOrThrow(fileId) {
|
|
723
|
+
const file2 = await this.getFile(fileId);
|
|
724
|
+
if (!file2) {
|
|
725
|
+
throw new Error(`File with id "${fileId}" not found`);
|
|
726
|
+
}
|
|
727
|
+
return file2;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Update file metadata
|
|
731
|
+
*
|
|
732
|
+
* @param fileId - File UUID
|
|
733
|
+
* @param data - Data to update
|
|
734
|
+
* @returns Updated file
|
|
735
|
+
*/
|
|
736
|
+
async updateFile(fileId, data) {
|
|
737
|
+
await this.getFileOrThrow(fileId);
|
|
738
|
+
return await this.adapter.files.update(fileId, data);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Delete file (soft delete)
|
|
742
|
+
*
|
|
743
|
+
* @param fileId - File UUID
|
|
744
|
+
* @param options - Delete options
|
|
745
|
+
*/
|
|
746
|
+
async deleteFile(fileId, options) {
|
|
747
|
+
if (options?.checkOwnership && options.userId) {
|
|
748
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
749
|
+
if (file2.uploadedBy !== options.userId) {
|
|
750
|
+
throw new Error("You can only delete files you uploaded");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (options?.hard) {
|
|
754
|
+
await this.adapter.files.hardDelete(fileId);
|
|
755
|
+
} else {
|
|
756
|
+
await this.adapter.files.delete(fileId);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* List files for the tenant
|
|
761
|
+
*/
|
|
762
|
+
async listFiles(options) {
|
|
763
|
+
return await this.adapter.files.list(this.tenantId, options);
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* List files by folder
|
|
767
|
+
*/
|
|
768
|
+
async listFilesByFolder(folderPath) {
|
|
769
|
+
return await this.adapter.files.findByFolder(this.tenantId, folderPath);
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* List files uploaded by a specific user
|
|
773
|
+
*/
|
|
774
|
+
async listFilesByUploader(uploadedBy) {
|
|
775
|
+
return await this.adapter.files.findByUploader(uploadedBy);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Change file visibility
|
|
779
|
+
*
|
|
780
|
+
* @param fileId - File UUID
|
|
781
|
+
* @param visibility - New visibility level
|
|
782
|
+
* @param allowedUsers - Users allowed to access (if restricted)
|
|
783
|
+
*/
|
|
784
|
+
async changeVisibility(fileId, visibility, allowedUsers) {
|
|
785
|
+
const updateData = { visibility };
|
|
786
|
+
if (visibility === "restricted" && allowedUsers) {
|
|
787
|
+
updateData.allowedUsers = allowedUsers;
|
|
788
|
+
}
|
|
789
|
+
return await this.updateFile(fileId, updateData);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Grant access to a file for specific users
|
|
793
|
+
*
|
|
794
|
+
* @param fileId - File UUID
|
|
795
|
+
* @param userIds - User IDs to grant access
|
|
796
|
+
*/
|
|
797
|
+
async grantAccess(fileId, userIds) {
|
|
798
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
799
|
+
const currentAllowed = file2.allowedUsers || [];
|
|
800
|
+
const newAllowed = [.../* @__PURE__ */ new Set([...currentAllowed, ...userIds])];
|
|
801
|
+
return await this.updateFile(fileId, {
|
|
802
|
+
visibility: "restricted",
|
|
803
|
+
allowedUsers: newAllowed
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Revoke access to a file for specific users
|
|
808
|
+
*
|
|
809
|
+
* @param fileId - File UUID
|
|
810
|
+
* @param userIds - User IDs to revoke access
|
|
811
|
+
*/
|
|
812
|
+
async revokeAccess(fileId, userIds) {
|
|
813
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
814
|
+
const currentAllowed = file2.allowedUsers || [];
|
|
815
|
+
const newAllowed = currentAllowed.filter((id) => !userIds.includes(id));
|
|
816
|
+
return await this.updateFile(fileId, {
|
|
817
|
+
allowedUsers: newAllowed
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Check if user has access to a file
|
|
822
|
+
*
|
|
823
|
+
* @param fileId - File UUID
|
|
824
|
+
* @param userId - User ID to check
|
|
825
|
+
* @returns true if user can access the file
|
|
826
|
+
*/
|
|
827
|
+
async canAccess(fileId, userId) {
|
|
828
|
+
const file2 = await this.getFile(fileId);
|
|
829
|
+
if (!file2) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
if (file2.visibility === "public") {
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
if (file2.uploadedBy === userId) {
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
if (file2.visibility === "restricted") {
|
|
839
|
+
return file2.allowedUsers?.includes(userId) ?? false;
|
|
840
|
+
}
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Move file to different folder
|
|
845
|
+
*/
|
|
846
|
+
async moveToFolder(fileId, newFolderPath) {
|
|
847
|
+
return await this.updateFile(fileId, { folderPath: newFolderPath });
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Add tags to file
|
|
851
|
+
*/
|
|
852
|
+
async addTags(fileId, tags) {
|
|
853
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
854
|
+
const currentTags = file2.tags || [];
|
|
855
|
+
const newTags = [.../* @__PURE__ */ new Set([...currentTags, ...tags])];
|
|
856
|
+
return await this.updateFile(fileId, { tags: newTags });
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Remove tags from file
|
|
860
|
+
*/
|
|
861
|
+
async removeTags(fileId, tags) {
|
|
862
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
863
|
+
const currentTags = file2.tags || [];
|
|
864
|
+
const newTags = currentTags.filter((tag) => !tags.includes(tag));
|
|
865
|
+
return await this.updateFile(fileId, { tags: newTags });
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// src/runtime/services/geocoding.service.ts
|
|
870
|
+
var GeocodingService = class {
|
|
871
|
+
constructor(adapter) {
|
|
872
|
+
this.adapter = adapter;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Search for address suggestions as the user types
|
|
876
|
+
*/
|
|
877
|
+
async autocomplete(params) {
|
|
878
|
+
if (!params.query || params.query.trim().length < 2) {
|
|
879
|
+
return [];
|
|
880
|
+
}
|
|
881
|
+
return await this.adapter.autocomplete({
|
|
882
|
+
...params,
|
|
883
|
+
query: params.query.trim(),
|
|
884
|
+
limit: params.limit ?? 5
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Reverse geocode coordinates to an address
|
|
889
|
+
*/
|
|
890
|
+
async reverse(params) {
|
|
891
|
+
if (!this.adapter.reverse) {
|
|
892
|
+
throw new Error("Reverse geocoding is not supported by the configured adapter");
|
|
893
|
+
}
|
|
894
|
+
return await this.adapter.reverse(params);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Geocode a structured address to coordinates
|
|
898
|
+
*/
|
|
899
|
+
async geocode(params) {
|
|
900
|
+
if (!this.adapter.geocode) {
|
|
901
|
+
throw new Error("Geocoding is not supported by the configured adapter");
|
|
902
|
+
}
|
|
903
|
+
return await this.adapter.geocode(params);
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// src/runtime/services/global-search.service.ts
|
|
908
|
+
var GlobalSearchService = class {
|
|
909
|
+
constructor(adapter, tenantId) {
|
|
910
|
+
this.adapter = adapter;
|
|
911
|
+
this.tenantId = tenantId;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Search across all object records for the tenant
|
|
915
|
+
*
|
|
916
|
+
* @param query - Search query string
|
|
917
|
+
* @param options - Search options (pagination, object filters)
|
|
918
|
+
* @returns Matching records with object metadata and total count
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* ```typescript
|
|
922
|
+
* // Basic search
|
|
923
|
+
* const { results, total } = await service.search("nike air");
|
|
924
|
+
*
|
|
925
|
+
* // With pagination
|
|
926
|
+
* const { results, total } = await service.search("nike", {
|
|
927
|
+
* limit: 10,
|
|
928
|
+
* offset: 20
|
|
929
|
+
* });
|
|
930
|
+
*
|
|
931
|
+
* // Filter by object types
|
|
932
|
+
* const { results, total } = await service.search("nike", {
|
|
933
|
+
* objectNames: ["products", "orders"]
|
|
934
|
+
* });
|
|
935
|
+
* ```
|
|
936
|
+
*/
|
|
937
|
+
async search(query, options) {
|
|
938
|
+
if (!query || query.trim().length === 0) {
|
|
939
|
+
return { results: [], total: 0 };
|
|
940
|
+
}
|
|
941
|
+
return await this.adapter.objectRecords.globalSearch(this.tenantId, query.trim(), {
|
|
942
|
+
limit: options?.limit ?? 20,
|
|
943
|
+
offset: options?.offset ?? 0,
|
|
944
|
+
objectNames: options?.objectNames,
|
|
945
|
+
includeObjectInfo: options?.includeObjectInfo ?? true
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Search and group results by object type
|
|
950
|
+
*
|
|
951
|
+
* @param query - Search query string
|
|
952
|
+
* @param options - Search options
|
|
953
|
+
* @returns Results grouped by object name
|
|
954
|
+
*/
|
|
955
|
+
async searchGrouped(query, options) {
|
|
956
|
+
const { results, total } = await this.search(query, {
|
|
957
|
+
...options,
|
|
958
|
+
limit: 100,
|
|
959
|
+
// Get more for grouping
|
|
960
|
+
offset: 0
|
|
961
|
+
});
|
|
962
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
963
|
+
for (const result of results) {
|
|
964
|
+
const existing = groupMap.get(result.objectName);
|
|
965
|
+
if (existing) {
|
|
966
|
+
existing.results.push(result);
|
|
967
|
+
} else {
|
|
968
|
+
groupMap.set(result.objectName, {
|
|
969
|
+
objectName: result.objectName,
|
|
970
|
+
objectLabel: result.objectLabel,
|
|
971
|
+
results: [result]
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
const groups = Array.from(groupMap.values()).map((g) => ({
|
|
976
|
+
...g,
|
|
977
|
+
count: g.results.length
|
|
978
|
+
}));
|
|
979
|
+
groups.sort((a, b) => b.count - a.count);
|
|
980
|
+
return { groups, total };
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// src/builders/attribute-validators.ts
|
|
985
|
+
import { ICONS } from "@andyoucreate/constants";
|
|
986
|
+
var VALID_ICONS = new Set(ICONS);
|
|
987
|
+
var ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
988
|
+
var MAX_NAME_LENGTH = 63;
|
|
989
|
+
var MAX_LABEL_LENGTH = 128;
|
|
990
|
+
function validateAttributeName(name) {
|
|
991
|
+
if (!name || name.length === 0) {
|
|
992
|
+
throw new Error("[AttributeBuilder] Attribute name cannot be empty");
|
|
993
|
+
}
|
|
994
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
995
|
+
throw new Error(
|
|
996
|
+
`[AttributeBuilder] Attribute name is too long (max ${MAX_NAME_LENGTH} characters)`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (!ATTRIBUTE_NAME_REGEX.test(name)) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
"[AttributeBuilder] Invalid attribute name format.\nName must be a valid variable identifier:\n \u2705 Valid: 'firstName', 'first_name', 'FirstName', 'FIRST_NAME'\n \u274C Invalid: 'first-name', 'first name', '123name', 'first.name'"
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function validateLabel(label) {
|
|
1006
|
+
if (!label || label.length === 0) {
|
|
1007
|
+
throw new Error("[AttributeBuilder] Attribute label cannot be empty");
|
|
1008
|
+
}
|
|
1009
|
+
if (label.length > MAX_LABEL_LENGTH) {
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
`[AttributeBuilder] Attribute label is too long (max ${MAX_LABEL_LENGTH} characters)`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function validateIconName(icon, context) {
|
|
1016
|
+
if (!VALID_ICONS.has(icon)) {
|
|
1017
|
+
throw new Error(
|
|
1018
|
+
`[AttributeBuilder] Invalid icon "${icon}" in ${context}.
|
|
1019
|
+
Icon must be one of the predefined icons from @andyoucreate/constants.`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function validateOptions(options, attributeName) {
|
|
1024
|
+
if (options.length === 0) return;
|
|
1025
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1026
|
+
const values = /* @__PURE__ */ new Set();
|
|
1027
|
+
for (const option of options) {
|
|
1028
|
+
if (!option.id?.trim()) {
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
`[AttributeBuilder] Option in "${attributeName}" has an empty or missing id.`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
if (!option.value?.trim()) {
|
|
1034
|
+
throw new Error(
|
|
1035
|
+
`[AttributeBuilder] Option "${option.id}" in "${attributeName}" has an empty or missing value.`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
if (!option.label?.trim()) {
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
`[AttributeBuilder] Option "${option.id}" in "${attributeName}" has an empty or missing label.`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
if (ids.has(option.id)) {
|
|
1044
|
+
throw new Error(
|
|
1045
|
+
`[AttributeBuilder] Duplicate option id "${option.id}" in "${attributeName}".`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
ids.add(option.id);
|
|
1049
|
+
if (values.has(option.value)) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
`[AttributeBuilder] Duplicate option value "${option.value}" in "${attributeName}".`
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
values.add(option.value);
|
|
1055
|
+
if (option.icon) {
|
|
1056
|
+
validateIconName(option.icon, `option "${option.id}" of "${attributeName}"`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/builders/attribute-builders.ts
|
|
1062
|
+
var BaseAttributeBuilder = class {
|
|
1063
|
+
constructor(type, name, label) {
|
|
1064
|
+
validateAttributeName(name);
|
|
1065
|
+
validateLabel(label);
|
|
1066
|
+
this.attr = {
|
|
1067
|
+
name,
|
|
1068
|
+
label,
|
|
1069
|
+
type,
|
|
1070
|
+
required: false
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
setRequired(value) {
|
|
1074
|
+
this.attr.required = value;
|
|
1075
|
+
}
|
|
1076
|
+
system() {
|
|
1077
|
+
this.attr.system = true;
|
|
1078
|
+
return this;
|
|
1079
|
+
}
|
|
1080
|
+
disabled() {
|
|
1081
|
+
this.attr.disabled = true;
|
|
1082
|
+
return this;
|
|
1083
|
+
}
|
|
1084
|
+
hidden() {
|
|
1085
|
+
this.attr.hidden = true;
|
|
1086
|
+
return this;
|
|
1087
|
+
}
|
|
1088
|
+
placeholder(value) {
|
|
1089
|
+
this.attr.placeholder = value;
|
|
1090
|
+
return this;
|
|
1091
|
+
}
|
|
1092
|
+
description(value) {
|
|
1093
|
+
this.attr.description = value;
|
|
1094
|
+
return this;
|
|
1095
|
+
}
|
|
1096
|
+
icon(value) {
|
|
1097
|
+
validateIconName(value, `attribute "${this.attr.name}"`);
|
|
1098
|
+
this.attr.icon = value;
|
|
1099
|
+
return this;
|
|
1100
|
+
}
|
|
1101
|
+
order(value) {
|
|
1102
|
+
this.attr.order = value;
|
|
1103
|
+
return this;
|
|
1104
|
+
}
|
|
1105
|
+
defaultValue(value) {
|
|
1106
|
+
this.attr.defaultValue = value;
|
|
1107
|
+
return this;
|
|
1108
|
+
}
|
|
1109
|
+
metadata(value) {
|
|
1110
|
+
this.attr.metadata = value;
|
|
1111
|
+
return this;
|
|
1112
|
+
}
|
|
1113
|
+
build() {
|
|
1114
|
+
return this.attr;
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
var TextAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1118
|
+
constructor(name, label) {
|
|
1119
|
+
super("text", name, label);
|
|
1120
|
+
}
|
|
1121
|
+
minLength(value) {
|
|
1122
|
+
this.attr.minLength = value;
|
|
1123
|
+
return this;
|
|
1124
|
+
}
|
|
1125
|
+
maxLength(value) {
|
|
1126
|
+
this.attr.maxLength = value;
|
|
1127
|
+
return this;
|
|
1128
|
+
}
|
|
1129
|
+
pattern(value) {
|
|
1130
|
+
this.attr.pattern = value;
|
|
1131
|
+
return this;
|
|
1132
|
+
}
|
|
1133
|
+
required() {
|
|
1134
|
+
this.setRequired(true);
|
|
1135
|
+
return this;
|
|
1136
|
+
}
|
|
1137
|
+
optional() {
|
|
1138
|
+
this.setRequired(false);
|
|
1139
|
+
return this;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Set email validation pattern
|
|
1143
|
+
*/
|
|
1144
|
+
email() {
|
|
1145
|
+
this.attr.pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
|
|
1146
|
+
return this;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Set URL validation pattern
|
|
1150
|
+
*/
|
|
1151
|
+
url() {
|
|
1152
|
+
this.attr.pattern = "^https?:\\/\\/[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$";
|
|
1153
|
+
return this;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Set slug validation pattern
|
|
1157
|
+
*/
|
|
1158
|
+
slug() {
|
|
1159
|
+
this.attr.pattern = "^[a-z0-9]+(?:-[a-z0-9]+)*$";
|
|
1160
|
+
return this;
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
function text(config) {
|
|
1164
|
+
return new TextAttributeBuilder(config.name, config.label);
|
|
1165
|
+
}
|
|
1166
|
+
var TextAreaAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1167
|
+
constructor(name, label) {
|
|
1168
|
+
super("textarea", name, label);
|
|
1169
|
+
}
|
|
1170
|
+
required() {
|
|
1171
|
+
this.setRequired(true);
|
|
1172
|
+
return this;
|
|
1173
|
+
}
|
|
1174
|
+
optional() {
|
|
1175
|
+
this.setRequired(false);
|
|
1176
|
+
return this;
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
function textarea(config) {
|
|
1180
|
+
return new TextAreaAttributeBuilder(config.name, config.label);
|
|
1181
|
+
}
|
|
1182
|
+
var NumberAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1183
|
+
constructor(name, label) {
|
|
1184
|
+
super("number", name, label);
|
|
1185
|
+
}
|
|
1186
|
+
min(value) {
|
|
1187
|
+
this.attr.min = value;
|
|
1188
|
+
return this;
|
|
1189
|
+
}
|
|
1190
|
+
max(value) {
|
|
1191
|
+
this.attr.max = value;
|
|
1192
|
+
return this;
|
|
1193
|
+
}
|
|
1194
|
+
integer() {
|
|
1195
|
+
this.attr.unit = "integer";
|
|
1196
|
+
this.attr.decimals = 0;
|
|
1197
|
+
return this;
|
|
1198
|
+
}
|
|
1199
|
+
decimal(decimals = 2) {
|
|
1200
|
+
this.attr.unit = "decimal";
|
|
1201
|
+
this.attr.decimals = decimals;
|
|
1202
|
+
return this;
|
|
1203
|
+
}
|
|
1204
|
+
percentage() {
|
|
1205
|
+
this.attr.unit = "percentage";
|
|
1206
|
+
return this;
|
|
1207
|
+
}
|
|
1208
|
+
required() {
|
|
1209
|
+
this.setRequired(true);
|
|
1210
|
+
return this;
|
|
1211
|
+
}
|
|
1212
|
+
optional() {
|
|
1213
|
+
this.setRequired(false);
|
|
1214
|
+
return this;
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
function number(config) {
|
|
1218
|
+
return new NumberAttributeBuilder(config.name, config.label);
|
|
1219
|
+
}
|
|
1220
|
+
var CheckboxAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1221
|
+
constructor(name, label) {
|
|
1222
|
+
super("checkbox", name, label);
|
|
1223
|
+
}
|
|
1224
|
+
required() {
|
|
1225
|
+
this.setRequired(true);
|
|
1226
|
+
return this;
|
|
1227
|
+
}
|
|
1228
|
+
optional() {
|
|
1229
|
+
this.setRequired(false);
|
|
1230
|
+
return this;
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
function checkbox(config) {
|
|
1234
|
+
return new CheckboxAttributeBuilder(config.name, config.label);
|
|
1235
|
+
}
|
|
1236
|
+
var DateAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1237
|
+
constructor(name, label) {
|
|
1238
|
+
super("date", name, label);
|
|
1239
|
+
}
|
|
1240
|
+
format(value) {
|
|
1241
|
+
this.attr.dateFormat = value;
|
|
1242
|
+
return this;
|
|
1243
|
+
}
|
|
1244
|
+
minDate(value) {
|
|
1245
|
+
this.attr.minDate = value;
|
|
1246
|
+
return this;
|
|
1247
|
+
}
|
|
1248
|
+
maxDate(value) {
|
|
1249
|
+
this.attr.maxDate = value;
|
|
1250
|
+
return this;
|
|
1251
|
+
}
|
|
1252
|
+
required() {
|
|
1253
|
+
this.setRequired(true);
|
|
1254
|
+
return this;
|
|
1255
|
+
}
|
|
1256
|
+
optional() {
|
|
1257
|
+
this.setRequired(false);
|
|
1258
|
+
return this;
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
function date(config) {
|
|
1262
|
+
return new DateAttributeBuilder(config.name, config.label);
|
|
1263
|
+
}
|
|
1264
|
+
var PhoneAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1265
|
+
constructor(name, label) {
|
|
1266
|
+
super("phone", name, label);
|
|
1267
|
+
}
|
|
1268
|
+
defaultCountry(value) {
|
|
1269
|
+
this.attr.defaultCountryCode = value;
|
|
1270
|
+
return this;
|
|
1271
|
+
}
|
|
1272
|
+
required() {
|
|
1273
|
+
this.setRequired(true);
|
|
1274
|
+
return this;
|
|
1275
|
+
}
|
|
1276
|
+
optional() {
|
|
1277
|
+
this.setRequired(false);
|
|
1278
|
+
return this;
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
function phone(config) {
|
|
1282
|
+
return new PhoneAttributeBuilder(config.name, config.label);
|
|
1283
|
+
}
|
|
1284
|
+
var CurrencyAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1285
|
+
constructor(name, label) {
|
|
1286
|
+
super("currency", name, label);
|
|
1287
|
+
}
|
|
1288
|
+
defaultCurrency(value) {
|
|
1289
|
+
this.attr.defaultCurrency = value;
|
|
1290
|
+
return this;
|
|
1291
|
+
}
|
|
1292
|
+
allowedCurrencies(value) {
|
|
1293
|
+
this.attr.allowedCurrencies = value;
|
|
1294
|
+
return this;
|
|
1295
|
+
}
|
|
1296
|
+
required() {
|
|
1297
|
+
this.setRequired(true);
|
|
1298
|
+
return this;
|
|
1299
|
+
}
|
|
1300
|
+
optional() {
|
|
1301
|
+
this.setRequired(false);
|
|
1302
|
+
return this;
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
function currency(config) {
|
|
1306
|
+
return new CurrencyAttributeBuilder(config.name, config.label);
|
|
1307
|
+
}
|
|
1308
|
+
var StatusAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1309
|
+
constructor(name, label) {
|
|
1310
|
+
super("status", name, label);
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Set all options at once (preserves literal types for values)
|
|
1314
|
+
*/
|
|
1315
|
+
options(value) {
|
|
1316
|
+
validateOptions(value, this.attr.name ?? "unknown");
|
|
1317
|
+
this.attr.options = value;
|
|
1318
|
+
return this;
|
|
1319
|
+
}
|
|
1320
|
+
option(config) {
|
|
1321
|
+
if (!this.attr.options) {
|
|
1322
|
+
this.attr.options = [];
|
|
1323
|
+
}
|
|
1324
|
+
const allOptions = [...this.attr.options, config];
|
|
1325
|
+
validateOptions(allOptions, this.attr.name ?? "unknown");
|
|
1326
|
+
this.attr.options.push(config);
|
|
1327
|
+
return this;
|
|
1328
|
+
}
|
|
1329
|
+
required() {
|
|
1330
|
+
this.setRequired(true);
|
|
1331
|
+
return this;
|
|
1332
|
+
}
|
|
1333
|
+
optional() {
|
|
1334
|
+
this.setRequired(false);
|
|
1335
|
+
return this;
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
function status(config) {
|
|
1339
|
+
return new StatusAttributeBuilder(config.name, config.label);
|
|
1340
|
+
}
|
|
1341
|
+
var SelectAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1342
|
+
constructor(name, label) {
|
|
1343
|
+
super("select", name, label);
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Set all options at once (preserves literal types for values)
|
|
1347
|
+
*/
|
|
1348
|
+
options(value) {
|
|
1349
|
+
validateOptions(value, this.attr.name ?? "unknown");
|
|
1350
|
+
this.attr.options = value;
|
|
1351
|
+
return this;
|
|
1352
|
+
}
|
|
1353
|
+
option(config) {
|
|
1354
|
+
if (!this.attr.options) {
|
|
1355
|
+
this.attr.options = [];
|
|
1356
|
+
}
|
|
1357
|
+
const allOptions = [...this.attr.options, config];
|
|
1358
|
+
validateOptions(allOptions, this.attr.name ?? "unknown");
|
|
1359
|
+
this.attr.options.push(config);
|
|
1360
|
+
return this;
|
|
1361
|
+
}
|
|
1362
|
+
required() {
|
|
1363
|
+
this.setRequired(true);
|
|
1364
|
+
return this;
|
|
1365
|
+
}
|
|
1366
|
+
optional() {
|
|
1367
|
+
this.setRequired(false);
|
|
1368
|
+
return this;
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
function select(config) {
|
|
1372
|
+
return new SelectAttributeBuilder(config.name, config.label);
|
|
1373
|
+
}
|
|
1374
|
+
var MultiselectAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1375
|
+
constructor(name, label) {
|
|
1376
|
+
super("multiselect", name, label);
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Set all options at once (preserves literal types for values)
|
|
1380
|
+
*/
|
|
1381
|
+
options(value) {
|
|
1382
|
+
validateOptions(value, this.attr.name ?? "unknown");
|
|
1383
|
+
this.attr.options = value;
|
|
1384
|
+
return this;
|
|
1385
|
+
}
|
|
1386
|
+
option(config) {
|
|
1387
|
+
if (!this.attr.options) {
|
|
1388
|
+
this.attr.options = [];
|
|
1389
|
+
}
|
|
1390
|
+
const allOptions = [...this.attr.options, config];
|
|
1391
|
+
validateOptions(allOptions, this.attr.name ?? "unknown");
|
|
1392
|
+
this.attr.options.push(config);
|
|
1393
|
+
return this;
|
|
1394
|
+
}
|
|
1395
|
+
required() {
|
|
1396
|
+
this.setRequired(true);
|
|
1397
|
+
return this;
|
|
1398
|
+
}
|
|
1399
|
+
optional() {
|
|
1400
|
+
this.setRequired(false);
|
|
1401
|
+
return this;
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
function multiselect(config) {
|
|
1405
|
+
return new MultiselectAttributeBuilder(config.name, config.label);
|
|
1406
|
+
}
|
|
1407
|
+
var LocationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1408
|
+
constructor(name, label) {
|
|
1409
|
+
super("location", name, label);
|
|
1410
|
+
}
|
|
1411
|
+
granularity(value) {
|
|
1412
|
+
this.attr.granularity = value;
|
|
1413
|
+
return this;
|
|
1414
|
+
}
|
|
1415
|
+
enableAutocomplete() {
|
|
1416
|
+
this.attr.enableAutocomplete = true;
|
|
1417
|
+
return this;
|
|
1418
|
+
}
|
|
1419
|
+
enableMap() {
|
|
1420
|
+
this.attr.enableMap = true;
|
|
1421
|
+
return this;
|
|
1422
|
+
}
|
|
1423
|
+
defaultCountry(value) {
|
|
1424
|
+
this.attr.defaultCountry = value;
|
|
1425
|
+
return this;
|
|
1426
|
+
}
|
|
1427
|
+
allowedCountries(value) {
|
|
1428
|
+
this.attr.allowedCountries = value;
|
|
1429
|
+
return this;
|
|
1430
|
+
}
|
|
1431
|
+
displayFormat(value) {
|
|
1432
|
+
this.attr.displayFormat = value;
|
|
1433
|
+
return this;
|
|
1434
|
+
}
|
|
1435
|
+
required() {
|
|
1436
|
+
this.setRequired(true);
|
|
1437
|
+
return this;
|
|
1438
|
+
}
|
|
1439
|
+
optional() {
|
|
1440
|
+
this.setRequired(false);
|
|
1441
|
+
return this;
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
function location(config) {
|
|
1445
|
+
return new LocationAttributeBuilder(config.name, config.label);
|
|
1446
|
+
}
|
|
1447
|
+
var TimestampAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1448
|
+
constructor(name, label) {
|
|
1449
|
+
super("timestamp", name, label);
|
|
1450
|
+
}
|
|
1451
|
+
autoUpdate() {
|
|
1452
|
+
this.attr.autoUpdate = true;
|
|
1453
|
+
return this;
|
|
1454
|
+
}
|
|
1455
|
+
required() {
|
|
1456
|
+
this.setRequired(true);
|
|
1457
|
+
return this;
|
|
1458
|
+
}
|
|
1459
|
+
optional() {
|
|
1460
|
+
this.setRequired(false);
|
|
1461
|
+
return this;
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
function timestamp(config) {
|
|
1465
|
+
return new TimestampAttributeBuilder(config.name, config.label);
|
|
1466
|
+
}
|
|
1467
|
+
var FileAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1468
|
+
constructor(name, label) {
|
|
1469
|
+
super("file", name, label);
|
|
1470
|
+
}
|
|
1471
|
+
maxFiles(value) {
|
|
1472
|
+
this.attr.maxFiles = value;
|
|
1473
|
+
return this;
|
|
1474
|
+
}
|
|
1475
|
+
maxSize(bytes) {
|
|
1476
|
+
this.attr.maxSize = bytes;
|
|
1477
|
+
return this;
|
|
1478
|
+
}
|
|
1479
|
+
allowedTypes(types) {
|
|
1480
|
+
this.attr.allowedTypes = types;
|
|
1481
|
+
return this;
|
|
1482
|
+
}
|
|
1483
|
+
verification(config) {
|
|
1484
|
+
this.attr.verification = config;
|
|
1485
|
+
return this;
|
|
1486
|
+
}
|
|
1487
|
+
required() {
|
|
1488
|
+
this.setRequired(true);
|
|
1489
|
+
return this;
|
|
1490
|
+
}
|
|
1491
|
+
optional() {
|
|
1492
|
+
this.setRequired(false);
|
|
1493
|
+
return this;
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
function file(config) {
|
|
1497
|
+
return new FileAttributeBuilder(config.name, config.label);
|
|
1498
|
+
}
|
|
1499
|
+
var UserAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1500
|
+
constructor(name, label) {
|
|
1501
|
+
super("user", name, label);
|
|
1502
|
+
}
|
|
1503
|
+
allowedRoles(roles) {
|
|
1504
|
+
this.attr.allowedRoles = roles;
|
|
1505
|
+
return this;
|
|
1506
|
+
}
|
|
1507
|
+
required() {
|
|
1508
|
+
this.setRequired(true);
|
|
1509
|
+
return this;
|
|
1510
|
+
}
|
|
1511
|
+
optional() {
|
|
1512
|
+
this.setRequired(false);
|
|
1513
|
+
return this;
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
function user(config) {
|
|
1517
|
+
return new UserAttributeBuilder(config.name, config.label);
|
|
1518
|
+
}
|
|
1519
|
+
var SingleRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1520
|
+
constructor(name, label) {
|
|
1521
|
+
super("relation", name, label);
|
|
1522
|
+
this.attr.cardinality = "one";
|
|
1523
|
+
this.attr.targets = [];
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Add a target object that can be linked
|
|
1527
|
+
* @param objectName - Name of the object (e.g., "companies")
|
|
1528
|
+
* @param options - Display template and filter options
|
|
1529
|
+
*/
|
|
1530
|
+
to(objectName, options) {
|
|
1531
|
+
const target = {
|
|
1532
|
+
object: objectName,
|
|
1533
|
+
...options
|
|
1534
|
+
};
|
|
1535
|
+
this.attr.targets?.push(target);
|
|
1536
|
+
return this;
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Set the deletion strategy when target record is deleted
|
|
1540
|
+
* @param strategy - "restrict" (default), "cascade", or "set_null"
|
|
1541
|
+
*/
|
|
1542
|
+
onDelete(strategy) {
|
|
1543
|
+
this.attr.onDelete = strategy;
|
|
1544
|
+
return this;
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Convert to a multi-relation (cardinality: "many")
|
|
1548
|
+
*/
|
|
1549
|
+
many() {
|
|
1550
|
+
const multiBuilder = new MultiRelationAttributeBuilder(
|
|
1551
|
+
this.attr.name,
|
|
1552
|
+
this.attr.label ?? "",
|
|
1553
|
+
{
|
|
1554
|
+
targets: this.attr.targets,
|
|
1555
|
+
onDelete: this.attr.onDelete,
|
|
1556
|
+
isRequired: this.attr.required
|
|
1557
|
+
}
|
|
1558
|
+
);
|
|
1559
|
+
return multiBuilder;
|
|
1560
|
+
}
|
|
1561
|
+
required() {
|
|
1562
|
+
this.setRequired(true);
|
|
1563
|
+
return this;
|
|
1564
|
+
}
|
|
1565
|
+
optional() {
|
|
1566
|
+
this.setRequired(false);
|
|
1567
|
+
return this;
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
var MultiRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1571
|
+
constructor(name, label, initOptions) {
|
|
1572
|
+
super("relation", name, label);
|
|
1573
|
+
this.attr.cardinality = "many";
|
|
1574
|
+
this.attr.targets = initOptions?.targets ?? [];
|
|
1575
|
+
this.attr.defaultValue = [];
|
|
1576
|
+
if (initOptions?.onDelete) {
|
|
1577
|
+
this.attr.onDelete = initOptions.onDelete;
|
|
1578
|
+
}
|
|
1579
|
+
if (initOptions?.isRequired) {
|
|
1580
|
+
this.setRequired(true);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Add a target object that can be linked
|
|
1585
|
+
* @param objectName - Name of the object (e.g., "companies")
|
|
1586
|
+
* @param options - Display template and filter options
|
|
1587
|
+
*/
|
|
1588
|
+
to(objectName, options) {
|
|
1589
|
+
const target = {
|
|
1590
|
+
object: objectName,
|
|
1591
|
+
...options
|
|
1592
|
+
};
|
|
1593
|
+
this.attr.targets?.push(target);
|
|
1594
|
+
return this;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Set the deletion strategy when target record is deleted
|
|
1598
|
+
* @param strategy - "restrict" (default), "cascade", or "set_null"
|
|
1599
|
+
*/
|
|
1600
|
+
onDelete(strategy) {
|
|
1601
|
+
this.attr.onDelete = strategy;
|
|
1602
|
+
return this;
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Set minimum number of relations required
|
|
1606
|
+
*/
|
|
1607
|
+
minItems(count) {
|
|
1608
|
+
this.attr.minItems = count;
|
|
1609
|
+
return this;
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Set maximum number of relations allowed
|
|
1613
|
+
*/
|
|
1614
|
+
maxItems(count) {
|
|
1615
|
+
this.attr.maxItems = count;
|
|
1616
|
+
return this;
|
|
1617
|
+
}
|
|
1618
|
+
required() {
|
|
1619
|
+
this.setRequired(true);
|
|
1620
|
+
return this;
|
|
1621
|
+
}
|
|
1622
|
+
optional() {
|
|
1623
|
+
this.setRequired(false);
|
|
1624
|
+
return this;
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
function relation(config) {
|
|
1628
|
+
return new SingleRelationAttributeBuilder(config.name, config.label);
|
|
1629
|
+
}
|
|
1630
|
+
var RatingAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1631
|
+
constructor(name, label) {
|
|
1632
|
+
super("rating", name, label);
|
|
1633
|
+
}
|
|
1634
|
+
max(value) {
|
|
1635
|
+
this.attr.max = value;
|
|
1636
|
+
return this;
|
|
1637
|
+
}
|
|
1638
|
+
iconType(value) {
|
|
1639
|
+
this.attr.iconType = value;
|
|
1640
|
+
return this;
|
|
1641
|
+
}
|
|
1642
|
+
required() {
|
|
1643
|
+
this.setRequired(true);
|
|
1644
|
+
return this;
|
|
1645
|
+
}
|
|
1646
|
+
optional() {
|
|
1647
|
+
this.setRequired(false);
|
|
1648
|
+
return this;
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
function rating(config) {
|
|
1652
|
+
return new RatingAttributeBuilder(config.name, config.label);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/builders/object-builder.ts
|
|
1656
|
+
import z from "zod";
|
|
1657
|
+
var ObjectBuilder = class {
|
|
1658
|
+
constructor(config) {
|
|
1659
|
+
this.validateName(config.name);
|
|
1660
|
+
this.obj = {
|
|
1661
|
+
name: config.name,
|
|
1662
|
+
label: config.label,
|
|
1663
|
+
attributes: [],
|
|
1664
|
+
system: false
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Set object description
|
|
1669
|
+
*/
|
|
1670
|
+
description(value) {
|
|
1671
|
+
this.obj.description = value;
|
|
1672
|
+
return this;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Set object icon
|
|
1676
|
+
*/
|
|
1677
|
+
icon(value) {
|
|
1678
|
+
this.obj.icon = value;
|
|
1679
|
+
return this;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Mark as system/native object (defined in code, protected)
|
|
1683
|
+
* System objects cannot be deleted/modified by clients
|
|
1684
|
+
*/
|
|
1685
|
+
system() {
|
|
1686
|
+
this.obj.system = true;
|
|
1687
|
+
return this;
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Add an attribute to the object with type accumulation
|
|
1691
|
+
*
|
|
1692
|
+
* The return type accumulates the attribute's name and value type
|
|
1693
|
+
* into the TRecord generic, enabling full inference.
|
|
1694
|
+
*
|
|
1695
|
+
* @param attr - Attribute definition or builder (auto-builds if builder)
|
|
1696
|
+
*/
|
|
1697
|
+
attribute(attr) {
|
|
1698
|
+
if (!this.obj.attributes) {
|
|
1699
|
+
this.obj.attributes = [];
|
|
1700
|
+
}
|
|
1701
|
+
const finalAttr = "build" in attr && typeof attr.build === "function" ? attr.build() : attr;
|
|
1702
|
+
this.obj.attributes.push(finalAttr);
|
|
1703
|
+
return this;
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Add multiple attributes at once
|
|
1707
|
+
* Note: For full type inference, prefer chaining .attribute() calls
|
|
1708
|
+
*/
|
|
1709
|
+
attributes(attrs) {
|
|
1710
|
+
if (!this.obj.attributes) {
|
|
1711
|
+
this.obj.attributes = [];
|
|
1712
|
+
}
|
|
1713
|
+
const finalAttrs = attrs.map((attr) => "build" in attr ? attr.build() : attr);
|
|
1714
|
+
this.obj.attributes.push(...finalAttrs);
|
|
1715
|
+
return this;
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Set metadata
|
|
1719
|
+
*/
|
|
1720
|
+
metadata(value) {
|
|
1721
|
+
this.obj.metadata = value;
|
|
1722
|
+
return this;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Set the label expression template for displaying records
|
|
1726
|
+
* This is REQUIRED for every object.
|
|
1727
|
+
*
|
|
1728
|
+
* Supports variable interpolation with `{{ attribute }}` syntax
|
|
1729
|
+
* and pipes for formatting: `{{ name | UPPER }}`, `{{ name | capitalize }}`
|
|
1730
|
+
*
|
|
1731
|
+
* Available pipes: UPPER, LOWER, capitalize, trim
|
|
1732
|
+
*
|
|
1733
|
+
* @param template - The label expression template
|
|
1734
|
+
*
|
|
1735
|
+
* @example
|
|
1736
|
+
* ```typescript
|
|
1737
|
+
* object({ name: "contacts", label: "Contact" })
|
|
1738
|
+
* .labelExpression("{{ firstName }} {{ lastName }}")
|
|
1739
|
+
* .attribute(text({ name: "firstName", label: "First Name" }).required())
|
|
1740
|
+
* .attribute(text({ name: "lastName", label: "Last Name" }).required())
|
|
1741
|
+
* ```
|
|
1742
|
+
*/
|
|
1743
|
+
labelExpression(template) {
|
|
1744
|
+
this._labelExpression = template;
|
|
1745
|
+
return this;
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Build the final ObjectDefinition
|
|
1749
|
+
* @throws Error if required fields are missing or invalid
|
|
1750
|
+
*/
|
|
1751
|
+
build() {
|
|
1752
|
+
if (!this.obj.name) {
|
|
1753
|
+
throw new Error("[ObjectBuilder] Missing required field: name");
|
|
1754
|
+
}
|
|
1755
|
+
if (!this.obj.label) {
|
|
1756
|
+
throw new Error("[ObjectBuilder] Missing required field: label");
|
|
1757
|
+
}
|
|
1758
|
+
if (!this._labelExpression) {
|
|
1759
|
+
throw new Error(
|
|
1760
|
+
`[ObjectBuilder] Object "${this.obj.name}" must have a labelExpression.
|
|
1761
|
+
The labelExpression defines how records are displayed in lists and relations.
|
|
1762
|
+
\u2705 Example: .labelExpression("{{ name }}")
|
|
1763
|
+
\u2705 Example: .labelExpression("{{ firstName }} {{ lastName }}")`
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
return {
|
|
1767
|
+
...this.obj,
|
|
1768
|
+
labelExpression: this._labelExpression
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Validate object name format
|
|
1773
|
+
* Must be kebab-case (slug format), max 63 characters
|
|
1774
|
+
* @internal
|
|
1775
|
+
*/
|
|
1776
|
+
validateName(name) {
|
|
1777
|
+
if (name === void 0) {
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
const objectNameSchema = z.string().min(1, "Object name cannot be empty").max(63, "Object name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
|
|
1781
|
+
message: "Invalid object name format.\nName must be in kebab-case (slug format):\n \u2705 Valid: 'products', 'user-profiles', 'order-items'\n \u274C Invalid: 'Products', 'userProfiles', 'user_profiles'"
|
|
1782
|
+
});
|
|
1783
|
+
try {
|
|
1784
|
+
objectNameSchema.parse(name);
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
if (error instanceof z.ZodError) {
|
|
1787
|
+
throw new Error(`[ObjectBuilder] ${error.errors[0].message}`);
|
|
1788
|
+
}
|
|
1789
|
+
throw error;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
function object(config) {
|
|
1794
|
+
return new ObjectBuilder(config);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// src/builders/view-builder.ts
|
|
1798
|
+
import { z as z2 } from "zod";
|
|
1799
|
+
var GroupBuilder = class {
|
|
1800
|
+
constructor(id, label) {
|
|
1801
|
+
this.data = { fields: [] };
|
|
1802
|
+
this.data.id = id;
|
|
1803
|
+
this.data.label = label;
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Set group description
|
|
1807
|
+
*/
|
|
1808
|
+
description(value) {
|
|
1809
|
+
this.data.description = value;
|
|
1810
|
+
return this;
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Make group collapsible
|
|
1814
|
+
* @param collapsed - Initial collapsed state (default: false)
|
|
1815
|
+
*/
|
|
1816
|
+
collapsible(collapsed = false) {
|
|
1817
|
+
this.data.collapsible = true;
|
|
1818
|
+
this.data.collapsed = collapsed;
|
|
1819
|
+
return this;
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Set display order
|
|
1823
|
+
*/
|
|
1824
|
+
order(value) {
|
|
1825
|
+
this.data.order = value;
|
|
1826
|
+
return this;
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Add multiple fields with default options
|
|
1830
|
+
* @example .fields("name", "email", "phone")
|
|
1831
|
+
*/
|
|
1832
|
+
fields(...names) {
|
|
1833
|
+
for (const name of names) {
|
|
1834
|
+
this.data.fields?.push({ attribute: name });
|
|
1835
|
+
}
|
|
1836
|
+
return this;
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Add a single field with options
|
|
1840
|
+
* @example .field("name", { span: 8, readOnly: true })
|
|
1841
|
+
*/
|
|
1842
|
+
field(attribute, options) {
|
|
1843
|
+
this.data.fields?.push({ attribute, ...options });
|
|
1844
|
+
return this;
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Add an inline attribute group (composite field with dropdown)
|
|
1848
|
+
* Groups multiple attributes into a single field
|
|
1849
|
+
* @example .attributeGroup({ id: "address", label: "Address", attributes: ["street", "city", "postal_code"], displayTemplate: "{street}, {city}" })
|
|
1850
|
+
*/
|
|
1851
|
+
attributeGroup(config, options) {
|
|
1852
|
+
this.data.fields?.push({ attributeGroup: config, ...options });
|
|
1853
|
+
return this;
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Build the group definition
|
|
1857
|
+
*/
|
|
1858
|
+
build() {
|
|
1859
|
+
if (!this.data.id) {
|
|
1860
|
+
throw new Error("[GroupBuilder] id is required");
|
|
1861
|
+
}
|
|
1862
|
+
if (!this.data.label) {
|
|
1863
|
+
throw new Error("[GroupBuilder] label is required");
|
|
1864
|
+
}
|
|
1865
|
+
if (!this.data.fields || this.data.fields.length === 0) {
|
|
1866
|
+
throw new Error("[GroupBuilder] At least one field is required");
|
|
1867
|
+
}
|
|
1868
|
+
return this.data;
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
var TableTabConfig = class {
|
|
1872
|
+
/** @internal */
|
|
1873
|
+
constructor(view2, base, relation2) {
|
|
1874
|
+
this.view = view2;
|
|
1875
|
+
this.tabData = { ...base, type: "table", relation: relation2, columns: [] };
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Set columns to display
|
|
1879
|
+
* @example .columns("firstName", "lastName", "email")
|
|
1880
|
+
*/
|
|
1881
|
+
columns(...names) {
|
|
1882
|
+
this.tabData.columns = names;
|
|
1883
|
+
return this;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Allow creating new records
|
|
1887
|
+
*/
|
|
1888
|
+
create() {
|
|
1889
|
+
this.tabData.allowCreate = true;
|
|
1890
|
+
return this;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Allow inline editing
|
|
1894
|
+
*/
|
|
1895
|
+
edit() {
|
|
1896
|
+
this.tabData.allowEdit = true;
|
|
1897
|
+
return this;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Allow deleting records
|
|
1901
|
+
*/
|
|
1902
|
+
delete() {
|
|
1903
|
+
this.tabData.allowDelete = true;
|
|
1904
|
+
return this;
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Enable all CRUD operations (create, edit, delete)
|
|
1908
|
+
*/
|
|
1909
|
+
crud() {
|
|
1910
|
+
this.tabData.allowCreate = true;
|
|
1911
|
+
this.tabData.allowEdit = true;
|
|
1912
|
+
this.tabData.allowDelete = true;
|
|
1913
|
+
return this;
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Set default filters
|
|
1917
|
+
*/
|
|
1918
|
+
filters(value) {
|
|
1919
|
+
this.tabData.filters = value;
|
|
1920
|
+
return this;
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Set default sort
|
|
1924
|
+
* @example .sort("lastName") or .sort("createdAt", "desc")
|
|
1925
|
+
*/
|
|
1926
|
+
sort(field, order = "asc") {
|
|
1927
|
+
this.tabData.sort = { field, order };
|
|
1928
|
+
return this;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Continue building with a new tab
|
|
1932
|
+
*/
|
|
1933
|
+
tab(name, label) {
|
|
1934
|
+
this.finalize();
|
|
1935
|
+
return new TabBuilder(this.view, name, label);
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Finish this tab and return to ViewBuilder
|
|
1939
|
+
*/
|
|
1940
|
+
done() {
|
|
1941
|
+
this.finalize();
|
|
1942
|
+
return this.view;
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Build the final view definition
|
|
1946
|
+
*/
|
|
1947
|
+
build() {
|
|
1948
|
+
this.finalize();
|
|
1949
|
+
return this.view.build();
|
|
1950
|
+
}
|
|
1951
|
+
finalize() {
|
|
1952
|
+
if (!this.tabData.columns || this.tabData.columns.length === 0) {
|
|
1953
|
+
throw new Error(`[TableTabConfig] columns() is required for tab "${this.tabData.name}"`);
|
|
1954
|
+
}
|
|
1955
|
+
this.view._addTab(this.tabData);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
var CustomTabConfig = class {
|
|
1959
|
+
/** @internal */
|
|
1960
|
+
constructor(view2, base, component) {
|
|
1961
|
+
this.view = view2;
|
|
1962
|
+
this.tabData = { ...base, type: "custom", component };
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Set props to pass to the component
|
|
1966
|
+
*/
|
|
1967
|
+
props(value) {
|
|
1968
|
+
this.tabData.props = value;
|
|
1969
|
+
return this;
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Continue building with a new tab
|
|
1973
|
+
*/
|
|
1974
|
+
tab(name, label) {
|
|
1975
|
+
this.view._addTab(this.tabData);
|
|
1976
|
+
return new TabBuilder(this.view, name, label);
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Finish this tab and return to ViewBuilder
|
|
1980
|
+
*/
|
|
1981
|
+
done() {
|
|
1982
|
+
this.view._addTab(this.tabData);
|
|
1983
|
+
return this.view;
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Build the final view definition
|
|
1987
|
+
*/
|
|
1988
|
+
build() {
|
|
1989
|
+
this.view._addTab(this.tabData);
|
|
1990
|
+
return this.view.build();
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
var TabBuilder = class {
|
|
1994
|
+
/** @internal */
|
|
1995
|
+
constructor(view2, name, label) {
|
|
1996
|
+
this.view = view2;
|
|
1997
|
+
this.base = {
|
|
1998
|
+
id: name,
|
|
1999
|
+
name,
|
|
2000
|
+
label
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Set tab icon
|
|
2005
|
+
*/
|
|
2006
|
+
icon(value) {
|
|
2007
|
+
this.base.icon = value;
|
|
2008
|
+
return this;
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Set display order
|
|
2012
|
+
*/
|
|
2013
|
+
order(value) {
|
|
2014
|
+
this.base.order = value;
|
|
2015
|
+
return this;
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Mark as system tab (protected)
|
|
2019
|
+
*/
|
|
2020
|
+
system() {
|
|
2021
|
+
this.base.system = true;
|
|
2022
|
+
return this;
|
|
2023
|
+
}
|
|
2024
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2025
|
+
// TYPE DISCRIMINATORS
|
|
2026
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2027
|
+
/**
|
|
2028
|
+
* Create a form tab with groups
|
|
2029
|
+
* @example .form(group("info", "Info").fields("name", "email"))
|
|
2030
|
+
*/
|
|
2031
|
+
form(...groups) {
|
|
2032
|
+
if (groups.length === 0) {
|
|
2033
|
+
throw new Error(
|
|
2034
|
+
`[TabBuilder] form() requires at least one group for tab "${this.base.name}"`
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
const tab = {
|
|
2038
|
+
...this.base,
|
|
2039
|
+
type: "form",
|
|
2040
|
+
groups: groups.map((g) => g instanceof GroupBuilder ? g.build() : g)
|
|
2041
|
+
};
|
|
2042
|
+
return this.view._addTab(tab);
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Create a table tab for a relation
|
|
2046
|
+
* @example .table("contacts").columns("name", "email").crud()
|
|
2047
|
+
*/
|
|
2048
|
+
table(relation2) {
|
|
2049
|
+
return new TableTabConfig(this.view, this.base, relation2);
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Create a custom tab with a component
|
|
2053
|
+
* @example .custom("AnalyticsWidget").props({ period: "12m" })
|
|
2054
|
+
*/
|
|
2055
|
+
custom(component) {
|
|
2056
|
+
return new CustomTabConfig(this.view, this.base, component);
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
var ViewBuilder = class {
|
|
2060
|
+
constructor(name, label) {
|
|
2061
|
+
this.data = { tabs: [] };
|
|
2062
|
+
this.validated = false;
|
|
2063
|
+
this.validateName(name);
|
|
2064
|
+
this.data.name = name;
|
|
2065
|
+
this.data.label = label;
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Set view description
|
|
2069
|
+
*/
|
|
2070
|
+
description(value) {
|
|
2071
|
+
this.data.description = value;
|
|
2072
|
+
return this;
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Set view icon
|
|
2076
|
+
*/
|
|
2077
|
+
icon(value) {
|
|
2078
|
+
this.data.icon = value;
|
|
2079
|
+
return this;
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Associate view with an object
|
|
2083
|
+
* @param objectName - Object name (kebab-case)
|
|
2084
|
+
*/
|
|
2085
|
+
for(objectName) {
|
|
2086
|
+
this.data.object = objectName;
|
|
2087
|
+
return this;
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Mark as default view for the object
|
|
2091
|
+
*/
|
|
2092
|
+
default() {
|
|
2093
|
+
this.data.default = true;
|
|
2094
|
+
return this;
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Mark as system view (protected, defined by developer)
|
|
2098
|
+
*/
|
|
2099
|
+
system() {
|
|
2100
|
+
this.data.system = true;
|
|
2101
|
+
return this;
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Set metadata
|
|
2105
|
+
*/
|
|
2106
|
+
metadata(value) {
|
|
2107
|
+
this.data.metadata = value;
|
|
2108
|
+
return this;
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Start building a new tab
|
|
2112
|
+
*/
|
|
2113
|
+
tab(name, label) {
|
|
2114
|
+
return new TabBuilder(this, name, label);
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Add a pre-built tab
|
|
2118
|
+
*/
|
|
2119
|
+
addTab(tab) {
|
|
2120
|
+
this.data.tabs?.push(tab);
|
|
2121
|
+
return this;
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* @internal Used by TabBuilder to add tabs
|
|
2125
|
+
*/
|
|
2126
|
+
_addTab(tab) {
|
|
2127
|
+
this.data.tabs?.push(tab);
|
|
2128
|
+
return this;
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Build the final view definition
|
|
2132
|
+
*/
|
|
2133
|
+
build() {
|
|
2134
|
+
if (!this.data.object) {
|
|
2135
|
+
throw new Error("[ViewBuilder] for() is required - specify the target object");
|
|
2136
|
+
}
|
|
2137
|
+
if (!this.data.tabs || this.data.tabs.length === 0) {
|
|
2138
|
+
throw new Error("[ViewBuilder] At least one tab is required");
|
|
2139
|
+
}
|
|
2140
|
+
const tabNames = /* @__PURE__ */ new Set();
|
|
2141
|
+
for (const tab of this.data.tabs) {
|
|
2142
|
+
if (tabNames.has(tab.name)) {
|
|
2143
|
+
throw new Error(`[ViewBuilder] Duplicate tab name "${tab.name}"`);
|
|
2144
|
+
}
|
|
2145
|
+
tabNames.add(tab.name);
|
|
2146
|
+
}
|
|
2147
|
+
this.validated = true;
|
|
2148
|
+
return this.data;
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Validate view name format (kebab-case)
|
|
2152
|
+
*/
|
|
2153
|
+
validateName(name) {
|
|
2154
|
+
const viewNameSchema = z2.string().min(1, "View name cannot be empty").max(63, "View name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
|
|
2155
|
+
message: "Invalid view name format.\nName must be in kebab-case:\n \u2705 Valid: 'detail', 'list-view', 'company-detail'\n \u274C Invalid: 'Detail', 'listView', 'list_view'"
|
|
2156
|
+
});
|
|
2157
|
+
try {
|
|
2158
|
+
viewNameSchema.parse(name);
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
if (error instanceof z2.ZodError) {
|
|
2161
|
+
throw new Error(`[ViewBuilder] ${error.errors[0].message}`);
|
|
2162
|
+
}
|
|
2163
|
+
throw error;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
function view(name, label) {
|
|
2168
|
+
return new ViewBuilder(name, label);
|
|
2169
|
+
}
|
|
2170
|
+
function group(id, label) {
|
|
2171
|
+
return new GroupBuilder(id, label);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// src/validation/validators.ts
|
|
2175
|
+
import { z as z3 } from "zod";
|
|
2176
|
+
var baseConfigSchema = z3.object({
|
|
2177
|
+
disabled: z3.boolean().optional(),
|
|
2178
|
+
placeholder: z3.string().optional(),
|
|
2179
|
+
description: z3.string().optional(),
|
|
2180
|
+
defaultValue: z3.unknown().optional(),
|
|
2181
|
+
icon: z3.string().optional(),
|
|
2182
|
+
order: z3.number().int().optional(),
|
|
2183
|
+
hidden: z3.boolean().optional(),
|
|
2184
|
+
archived: z3.boolean().optional(),
|
|
2185
|
+
deprecated: z3.boolean().optional(),
|
|
2186
|
+
metadata: z3.record(z3.unknown()).optional()
|
|
2187
|
+
});
|
|
2188
|
+
var optionSchema = z3.object({
|
|
2189
|
+
id: z3.string().min(1),
|
|
2190
|
+
label: z3.string().min(1),
|
|
2191
|
+
value: z3.string().min(1),
|
|
2192
|
+
color: z3.string().optional(),
|
|
2193
|
+
icon: z3.string().optional(),
|
|
2194
|
+
description: z3.string().optional(),
|
|
2195
|
+
group: z3.enum(["idle", "in_progress", "finished"]).optional()
|
|
2196
|
+
});
|
|
2197
|
+
var relationTargetSchema = z3.object({
|
|
2198
|
+
object: z3.string().min(1),
|
|
2199
|
+
displayTemplate: z3.string().optional(),
|
|
2200
|
+
filter: z3.record(z3.unknown()).optional()
|
|
2201
|
+
});
|
|
2202
|
+
var documentTypeConfigSchema = z3.object({
|
|
2203
|
+
type: z3.enum([
|
|
2204
|
+
"id_card",
|
|
2205
|
+
"passport",
|
|
2206
|
+
"incorporation_certificate",
|
|
2207
|
+
"driver_license",
|
|
2208
|
+
"birth_certificate",
|
|
2209
|
+
"residence_permit",
|
|
2210
|
+
"bank_statement",
|
|
2211
|
+
"proof_of_address",
|
|
2212
|
+
"custom"
|
|
2213
|
+
]),
|
|
2214
|
+
label: z3.string(),
|
|
2215
|
+
faces: z3.array(z3.enum(["front", "back", "single"])),
|
|
2216
|
+
attributeMapping: z3.array(
|
|
2217
|
+
z3.object({
|
|
2218
|
+
attributeId: z3.string(),
|
|
2219
|
+
extractedKey: z3.string(),
|
|
2220
|
+
face: z3.enum(["front", "back", "single"]).optional(),
|
|
2221
|
+
required: z3.boolean().optional()
|
|
2222
|
+
})
|
|
2223
|
+
).optional()
|
|
2224
|
+
});
|
|
2225
|
+
var fileVerificationConfigSchema = z3.object({
|
|
2226
|
+
enabled: z3.boolean(),
|
|
2227
|
+
documentTypes: z3.array(documentTypeConfigSchema),
|
|
2228
|
+
autoExtract: z3.boolean().optional(),
|
|
2229
|
+
autoValidate: z3.boolean().optional()
|
|
2230
|
+
});
|
|
2231
|
+
var textConfigSchema = baseConfigSchema.extend({
|
|
2232
|
+
minLength: z3.number().int().min(0).optional(),
|
|
2233
|
+
maxLength: z3.number().int().min(1).optional(),
|
|
2234
|
+
pattern: z3.string().optional()
|
|
2235
|
+
});
|
|
2236
|
+
var textareaConfigSchema = baseConfigSchema;
|
|
2237
|
+
var numberConfigSchema = baseConfigSchema.extend({
|
|
2238
|
+
min: z3.number().optional(),
|
|
2239
|
+
max: z3.number().optional(),
|
|
2240
|
+
unit: z3.enum(["integer", "decimal", "percentage"]).optional(),
|
|
2241
|
+
decimals: z3.number().int().min(0).optional()
|
|
2242
|
+
});
|
|
2243
|
+
var checkboxConfigSchema = baseConfigSchema;
|
|
2244
|
+
var dateConfigSchema = baseConfigSchema.extend({
|
|
2245
|
+
dateFormat: z3.enum(["short", "long", "full", "relative"]).optional(),
|
|
2246
|
+
minDate: z3.string().optional(),
|
|
2247
|
+
maxDate: z3.string().optional()
|
|
2248
|
+
});
|
|
2249
|
+
var phoneConfigSchema = baseConfigSchema.extend({
|
|
2250
|
+
defaultCountryCode: z3.string().length(3).optional()
|
|
2251
|
+
});
|
|
2252
|
+
var currencyConfigSchema = baseConfigSchema.extend({
|
|
2253
|
+
defaultCurrency: z3.string().length(3).optional(),
|
|
2254
|
+
allowedCurrencies: z3.array(z3.string().length(3)).optional()
|
|
2255
|
+
});
|
|
2256
|
+
var statusConfigSchema = baseConfigSchema.extend({
|
|
2257
|
+
options: z3.array(optionSchema).min(1)
|
|
2258
|
+
});
|
|
2259
|
+
var locationConfigSchema = baseConfigSchema.extend({
|
|
2260
|
+
granularity: z3.enum(["full", "address", "city", "state", "country", "coordinates"]),
|
|
2261
|
+
enableAutocomplete: z3.boolean().optional(),
|
|
2262
|
+
enableMap: z3.boolean().optional(),
|
|
2263
|
+
defaultCountry: z3.string().length(3).optional(),
|
|
2264
|
+
allowedCountries: z3.array(z3.string().length(3)).optional(),
|
|
2265
|
+
displayFormat: z3.enum(["single_line", "multi_line", "compact"]).optional()
|
|
2266
|
+
});
|
|
2267
|
+
var timestampConfigSchema = baseConfigSchema.extend({
|
|
2268
|
+
autoUpdate: z3.boolean().optional()
|
|
2269
|
+
});
|
|
2270
|
+
var selectConfigSchema = baseConfigSchema.extend({
|
|
2271
|
+
options: z3.array(optionSchema).min(1)
|
|
2272
|
+
});
|
|
2273
|
+
var multiselectConfigSchema = baseConfigSchema.extend({
|
|
2274
|
+
options: z3.array(optionSchema).min(1)
|
|
2275
|
+
});
|
|
2276
|
+
var fileConfigSchema = baseConfigSchema.extend({
|
|
2277
|
+
maxFiles: z3.number().int().min(1).optional(),
|
|
2278
|
+
maxSize: z3.number().int().min(1).optional(),
|
|
2279
|
+
allowedTypes: z3.array(z3.string()).optional(),
|
|
2280
|
+
verification: fileVerificationConfigSchema.optional()
|
|
2281
|
+
});
|
|
2282
|
+
var userConfigSchema = baseConfigSchema.extend({
|
|
2283
|
+
allowedRoles: z3.array(z3.string()).optional()
|
|
2284
|
+
});
|
|
2285
|
+
var relationConfigSchema = baseConfigSchema.extend({
|
|
2286
|
+
targets: z3.array(relationTargetSchema).min(1),
|
|
2287
|
+
onDelete: z3.enum(["restrict", "cascade", "set_null"]).optional(),
|
|
2288
|
+
cardinality: z3.enum(["one", "many"]),
|
|
2289
|
+
minItems: z3.number().int().min(0).optional(),
|
|
2290
|
+
maxItems: z3.number().int().min(1).optional()
|
|
2291
|
+
});
|
|
2292
|
+
var ratingConfigSchema = baseConfigSchema.extend({
|
|
2293
|
+
max: z3.number().int().min(1).optional(),
|
|
2294
|
+
iconType: z3.enum(["star", "heart", "thumbs", "number"]).optional()
|
|
2295
|
+
});
|
|
2296
|
+
var attributeConfigSchemas = {
|
|
2297
|
+
text: textConfigSchema,
|
|
2298
|
+
textarea: textareaConfigSchema,
|
|
2299
|
+
number: numberConfigSchema,
|
|
2300
|
+
checkbox: checkboxConfigSchema,
|
|
2301
|
+
date: dateConfigSchema,
|
|
2302
|
+
phone: phoneConfigSchema,
|
|
2303
|
+
currency: currencyConfigSchema,
|
|
2304
|
+
status: statusConfigSchema,
|
|
2305
|
+
location: locationConfigSchema,
|
|
2306
|
+
timestamp: timestampConfigSchema,
|
|
2307
|
+
select: selectConfigSchema,
|
|
2308
|
+
multiselect: multiselectConfigSchema,
|
|
2309
|
+
file: fileConfigSchema,
|
|
2310
|
+
user: userConfigSchema,
|
|
2311
|
+
relation: relationConfigSchema,
|
|
2312
|
+
rating: ratingConfigSchema
|
|
2313
|
+
};
|
|
2314
|
+
function getAttributeConfigSchema(type) {
|
|
2315
|
+
return attributeConfigSchemas[type];
|
|
2316
|
+
}
|
|
2317
|
+
function validateAttributeConfig(type, config) {
|
|
2318
|
+
const schema = getAttributeConfigSchema(type);
|
|
2319
|
+
const result = schema.safeParse(config);
|
|
2320
|
+
if (result.success) {
|
|
2321
|
+
return { success: true, data: result.data };
|
|
2322
|
+
}
|
|
2323
|
+
return {
|
|
2324
|
+
success: false,
|
|
2325
|
+
errors: result.error.errors.map((err) => `${err.path.join(".")}: ${err.message}`)
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
function parseAttributeConfig(type, config) {
|
|
2329
|
+
const schema = getAttributeConfigSchema(type);
|
|
2330
|
+
return schema.strip().parse(config);
|
|
2331
|
+
}
|
|
2332
|
+
function safeParseAttributeConfig(type, config) {
|
|
2333
|
+
const schema = getAttributeConfigSchema(type);
|
|
2334
|
+
const result = schema.strip().safeParse(config);
|
|
2335
|
+
return result.success ? result.data : void 0;
|
|
2336
|
+
}
|
|
2337
|
+
function createTextValidator(attr) {
|
|
2338
|
+
let schema = z3.string();
|
|
2339
|
+
if (attr.minLength !== void 0) {
|
|
2340
|
+
schema = schema.min(
|
|
2341
|
+
attr.minLength,
|
|
2342
|
+
`${attr.label} must be at least ${attr.minLength} characters`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
if (attr.maxLength !== void 0) {
|
|
2346
|
+
schema = schema.max(
|
|
2347
|
+
attr.maxLength,
|
|
2348
|
+
`${attr.label} must be at most ${attr.maxLength} characters`
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
if (attr.pattern) {
|
|
2352
|
+
schema = schema.regex(new RegExp(attr.pattern), `${attr.label} format is invalid`);
|
|
2353
|
+
}
|
|
2354
|
+
return schema;
|
|
2355
|
+
}
|
|
2356
|
+
function createNumberValidator(attr) {
|
|
2357
|
+
let schema = z3.number();
|
|
2358
|
+
if (attr.min !== void 0) {
|
|
2359
|
+
schema = schema.min(attr.min, `${attr.label} must be at least ${attr.min}`);
|
|
2360
|
+
}
|
|
2361
|
+
if (attr.max !== void 0) {
|
|
2362
|
+
schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
|
|
2363
|
+
}
|
|
2364
|
+
if (attr.unit === "integer") {
|
|
2365
|
+
schema = schema.int(`${attr.label} must be an integer`);
|
|
2366
|
+
}
|
|
2367
|
+
return schema;
|
|
2368
|
+
}
|
|
2369
|
+
function createCheckboxValidator(_attr) {
|
|
2370
|
+
return z3.boolean();
|
|
2371
|
+
}
|
|
2372
|
+
function createDateValidator(attr) {
|
|
2373
|
+
return z3.string().datetime({ message: `${attr.label} must be a valid ISO date` });
|
|
2374
|
+
}
|
|
2375
|
+
function createPhoneValidator(_attr) {
|
|
2376
|
+
return z3.object({
|
|
2377
|
+
countryCode: z3.string().length(3),
|
|
2378
|
+
phoneNumber: z3.string().min(1)
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
function createCurrencyValidator(_attr) {
|
|
2382
|
+
return z3.object({
|
|
2383
|
+
code: z3.string().length(3),
|
|
2384
|
+
value: z3.number().min(0)
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
function createStatusValidator(attr) {
|
|
2388
|
+
const validValues = attr.options.map((opt) => opt.value);
|
|
2389
|
+
return z3.enum(validValues, {
|
|
2390
|
+
errorMap: () => ({
|
|
2391
|
+
message: `${attr.label} must be one of: ${validValues.join(", ")}`
|
|
2392
|
+
})
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
function createSelectValidator(attr) {
|
|
2396
|
+
const validValues = attr.options.map((opt) => opt.value);
|
|
2397
|
+
return z3.enum(validValues, {
|
|
2398
|
+
errorMap: () => ({
|
|
2399
|
+
message: `${attr.label} must be one of: ${validValues.join(", ")}`
|
|
2400
|
+
})
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
function createMultiselectValidator(attr) {
|
|
2404
|
+
const validValues = attr.options.map((opt) => opt.value);
|
|
2405
|
+
return z3.array(
|
|
2406
|
+
z3.enum(validValues, {
|
|
2407
|
+
errorMap: () => ({
|
|
2408
|
+
message: `Each value must be one of: ${validValues.join(", ")}`
|
|
2409
|
+
})
|
|
2410
|
+
})
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
function createLocationValidator(_attr) {
|
|
2414
|
+
return z3.object({
|
|
2415
|
+
address: z3.string().optional(),
|
|
2416
|
+
address2: z3.string().optional(),
|
|
2417
|
+
city: z3.string().optional(),
|
|
2418
|
+
state: z3.string().optional(),
|
|
2419
|
+
postalCode: z3.string().optional(),
|
|
2420
|
+
country: z3.string().length(3).optional(),
|
|
2421
|
+
latitude: z3.number().optional(),
|
|
2422
|
+
longitude: z3.number().optional()
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
function createTimestampValidator(_attr) {
|
|
2426
|
+
return z3.number().int().positive();
|
|
2427
|
+
}
|
|
2428
|
+
function createFileValidator(_attr) {
|
|
2429
|
+
return z3.string().uuid();
|
|
2430
|
+
}
|
|
2431
|
+
function createUserValidator(_attr) {
|
|
2432
|
+
return z3.string().uuid();
|
|
2433
|
+
}
|
|
2434
|
+
function createSingleRelationValidator(attr) {
|
|
2435
|
+
const uuidSchema = z3.string().uuid({
|
|
2436
|
+
message: `${attr.label} must be a valid record ID`
|
|
2437
|
+
});
|
|
2438
|
+
return z3.union([uuidSchema, z3.null()]);
|
|
2439
|
+
}
|
|
2440
|
+
function createMultiRelationValidator(attr) {
|
|
2441
|
+
const uuidSchema = z3.string().uuid({
|
|
2442
|
+
message: `Each ${attr.label} item must be a valid record ID`
|
|
2443
|
+
});
|
|
2444
|
+
let arraySchema = z3.array(uuidSchema);
|
|
2445
|
+
if (attr.minItems !== void 0) {
|
|
2446
|
+
arraySchema = arraySchema.min(
|
|
2447
|
+
attr.minItems,
|
|
2448
|
+
`${attr.label} must have at least ${attr.minItems} item${attr.minItems > 1 ? "s" : ""}`
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
if (attr.maxItems !== void 0) {
|
|
2452
|
+
arraySchema = arraySchema.max(
|
|
2453
|
+
attr.maxItems,
|
|
2454
|
+
`${attr.label} must have at most ${attr.maxItems} item${attr.maxItems > 1 ? "s" : ""}`
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
return arraySchema;
|
|
2458
|
+
}
|
|
2459
|
+
function createRelationValidator(attr) {
|
|
2460
|
+
if (attr.cardinality === "many") {
|
|
2461
|
+
return createMultiRelationValidator(attr);
|
|
2462
|
+
}
|
|
2463
|
+
return createSingleRelationValidator(attr);
|
|
2464
|
+
}
|
|
2465
|
+
function createRatingValidator(attr) {
|
|
2466
|
+
let schema = z3.number().int().min(0);
|
|
2467
|
+
if (attr.max !== void 0) {
|
|
2468
|
+
schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
|
|
2469
|
+
}
|
|
2470
|
+
return schema;
|
|
2471
|
+
}
|
|
2472
|
+
function createAttributeValidator(attr) {
|
|
2473
|
+
switch (attr.type) {
|
|
2474
|
+
case "text":
|
|
2475
|
+
return createTextValidator(attr);
|
|
2476
|
+
case "number":
|
|
2477
|
+
return createNumberValidator(attr);
|
|
2478
|
+
case "checkbox":
|
|
2479
|
+
return createCheckboxValidator(attr);
|
|
2480
|
+
case "date":
|
|
2481
|
+
return createDateValidator(attr);
|
|
2482
|
+
case "phone":
|
|
2483
|
+
return createPhoneValidator(attr);
|
|
2484
|
+
case "currency":
|
|
2485
|
+
return createCurrencyValidator(attr);
|
|
2486
|
+
case "status":
|
|
2487
|
+
return createStatusValidator(attr);
|
|
2488
|
+
case "location":
|
|
2489
|
+
return createLocationValidator(attr);
|
|
2490
|
+
case "timestamp":
|
|
2491
|
+
return createTimestampValidator(attr);
|
|
2492
|
+
case "select":
|
|
2493
|
+
return createSelectValidator(attr);
|
|
2494
|
+
case "multiselect":
|
|
2495
|
+
return createMultiselectValidator(attr);
|
|
2496
|
+
case "file":
|
|
2497
|
+
return createFileValidator(attr);
|
|
2498
|
+
case "user":
|
|
2499
|
+
return createUserValidator(attr);
|
|
2500
|
+
case "relation":
|
|
2501
|
+
return createRelationValidator(attr);
|
|
2502
|
+
case "rating":
|
|
2503
|
+
return createRatingValidator(attr);
|
|
2504
|
+
default:
|
|
2505
|
+
return z3.unknown();
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
function createObjectValidator(objectDef) {
|
|
2509
|
+
const shape = {};
|
|
2510
|
+
for (const attr of objectDef.attributes) {
|
|
2511
|
+
let validator = createAttributeValidator(attr);
|
|
2512
|
+
if (!attr.required) {
|
|
2513
|
+
validator = validator.optional();
|
|
2514
|
+
}
|
|
2515
|
+
shape[attr.name] = validator;
|
|
2516
|
+
}
|
|
2517
|
+
return z3.object(shape);
|
|
2518
|
+
}
|
|
2519
|
+
function validateAttribute(attr, value) {
|
|
2520
|
+
const validator = createAttributeValidator(attr);
|
|
2521
|
+
if (!attr.required && (value === void 0 || value === null)) {
|
|
2522
|
+
return { success: true, data: { [attr.name]: value } };
|
|
2523
|
+
}
|
|
2524
|
+
const result = validator.safeParse(value);
|
|
2525
|
+
if (result.success) {
|
|
2526
|
+
return {
|
|
2527
|
+
success: true,
|
|
2528
|
+
data: { [attr.name]: result.data }
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
return {
|
|
2532
|
+
success: false,
|
|
2533
|
+
errors: result.error.errors.map((err) => ({
|
|
2534
|
+
path: [attr.name, ...err.path.map(String)],
|
|
2535
|
+
message: err.message
|
|
2536
|
+
}))
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
function validateObject(objectDef, data) {
|
|
2540
|
+
const validator = createObjectValidator(objectDef);
|
|
2541
|
+
const result = validator.safeParse(data);
|
|
2542
|
+
if (result.success) {
|
|
2543
|
+
return {
|
|
2544
|
+
success: true,
|
|
2545
|
+
data: result.data
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
return {
|
|
2549
|
+
success: false,
|
|
2550
|
+
errors: result.error.errors.map((err) => ({
|
|
2551
|
+
path: err.path.map(String),
|
|
2552
|
+
message: err.message
|
|
2553
|
+
}))
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
function validateObjectOrThrow(objectDef, data) {
|
|
2557
|
+
const result = validateObject(objectDef, data);
|
|
2558
|
+
if (!result.success) {
|
|
2559
|
+
const errorMessages = result.errors?.map((err) => `${err.path.join(".")}: ${err.message}`).join("\n") || "Unknown validation error";
|
|
2560
|
+
throw new Error(`Validation failed for ${objectDef.label}:
|
|
2561
|
+
${errorMessages}`);
|
|
2562
|
+
}
|
|
2563
|
+
return result.data;
|
|
2564
|
+
}
|
|
2565
|
+
function createDraftValidator(objectDef) {
|
|
2566
|
+
const shape = {};
|
|
2567
|
+
for (const attr of objectDef.attributes) {
|
|
2568
|
+
const validator = createAttributeValidator(attr).optional();
|
|
2569
|
+
shape[attr.name] = validator;
|
|
2570
|
+
}
|
|
2571
|
+
return z3.object(shape);
|
|
2572
|
+
}
|
|
2573
|
+
function validateDraft(objectDef, data) {
|
|
2574
|
+
const validator = createDraftValidator(objectDef);
|
|
2575
|
+
const result = validator.safeParse(data);
|
|
2576
|
+
if (result.success) {
|
|
2577
|
+
return {
|
|
2578
|
+
success: true,
|
|
2579
|
+
data: result.data
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
return {
|
|
2583
|
+
success: false,
|
|
2584
|
+
errors: result.error.errors.map((err) => ({
|
|
2585
|
+
path: err.path.map(String),
|
|
2586
|
+
message: err.message
|
|
2587
|
+
}))
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
function validateDraftOrThrow(objectDef, data) {
|
|
2591
|
+
const result = validateDraft(objectDef, data);
|
|
2592
|
+
if (!result.success) {
|
|
2593
|
+
const errorMessages = result.errors?.map((err) => `${err.path.join(".")}: ${err.message}`).join("\n") || "Unknown validation error";
|
|
2594
|
+
throw new Error(`Draft validation failed for ${objectDef.label}:
|
|
2595
|
+
${errorMessages}`);
|
|
2596
|
+
}
|
|
2597
|
+
return result.data;
|
|
2598
|
+
}
|
|
2599
|
+
function isValuePresent(value) {
|
|
2600
|
+
if (value === void 0 || value === null) {
|
|
2601
|
+
return false;
|
|
2602
|
+
}
|
|
2603
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
return true;
|
|
2607
|
+
}
|
|
2608
|
+
function getMissingRequiredAttributes(objectDef, data) {
|
|
2609
|
+
const missing = [];
|
|
2610
|
+
for (const attr of objectDef.attributes) {
|
|
2611
|
+
if (attr.required && !isValuePresent(data[attr.name])) {
|
|
2612
|
+
missing.push(attr);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
return missing;
|
|
2616
|
+
}
|
|
2617
|
+
function isRecordComplete(objectDef, data) {
|
|
2618
|
+
const missing = getMissingRequiredAttributes(objectDef, data);
|
|
2619
|
+
if (missing.length > 0) {
|
|
2620
|
+
return false;
|
|
2621
|
+
}
|
|
2622
|
+
const validation = validateObject(objectDef, data);
|
|
2623
|
+
return validation.success;
|
|
2624
|
+
}
|
|
2625
|
+
function computeRecordStatus(objectDef, data) {
|
|
2626
|
+
return isRecordComplete(objectDef, data) ? "complete" : "draft";
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// src/runtime/services/object-schema.service.ts
|
|
2630
|
+
var ObjectSchemaService = class {
|
|
2631
|
+
constructor(adapter, nativeRegistry) {
|
|
2632
|
+
this.adapter = adapter;
|
|
2633
|
+
this.nativeRegistry = nativeRegistry;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Create a new custom object
|
|
2637
|
+
* Validates name format and ensures system=false
|
|
2638
|
+
*
|
|
2639
|
+
* @param definition - Object definition (attributes can be raw definitions or builders)
|
|
2640
|
+
* @param tenantId - Tenant ID owning the object
|
|
2641
|
+
* @returns Created ObjectDefinition
|
|
2642
|
+
*/
|
|
2643
|
+
async createCustomObject(definition, tenantId) {
|
|
2644
|
+
const builder = object({
|
|
2645
|
+
name: definition.name,
|
|
2646
|
+
label: definition.label
|
|
2647
|
+
});
|
|
2648
|
+
if (definition.description) builder.description(definition.description);
|
|
2649
|
+
if (definition.icon) builder.icon(definition.icon);
|
|
2650
|
+
if (definition.metadata) builder.metadata(definition.metadata);
|
|
2651
|
+
builder.labelExpression(definition.labelExpression);
|
|
2652
|
+
if (definition.attributes && definition.attributes.length > 0) {
|
|
2653
|
+
builder.attributes(definition.attributes);
|
|
2654
|
+
}
|
|
2655
|
+
const objectDef = builder.build();
|
|
2656
|
+
const existing = await this.adapter.objects.findByName(tenantId, objectDef.name);
|
|
2657
|
+
if (existing) {
|
|
2658
|
+
throw new Error(`Object with name "${objectDef.name}" already exists`);
|
|
2659
|
+
}
|
|
2660
|
+
const validatedConfigs = [];
|
|
2661
|
+
if (objectDef.attributes.length > 0) {
|
|
2662
|
+
for (const attr of objectDef.attributes) {
|
|
2663
|
+
const config = await this.validateAttributeInput(
|
|
2664
|
+
attr,
|
|
2665
|
+
tenantId
|
|
2666
|
+
);
|
|
2667
|
+
validatedConfigs.push(config);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
const metadata = {
|
|
2671
|
+
...objectDef.metadata,
|
|
2672
|
+
icon: objectDef.icon
|
|
2673
|
+
};
|
|
2674
|
+
const dbObject = await this.adapter.objects.create({
|
|
2675
|
+
tenantId,
|
|
2676
|
+
name: objectDef.name,
|
|
2677
|
+
label: objectDef.label,
|
|
2678
|
+
description: objectDef.description,
|
|
2679
|
+
labelExpression: objectDef.labelExpression,
|
|
2680
|
+
system: false,
|
|
2681
|
+
// Always false for custom objects
|
|
2682
|
+
metadata
|
|
2683
|
+
});
|
|
2684
|
+
const createdAttributes = [];
|
|
2685
|
+
if (objectDef.attributes.length > 0) {
|
|
2686
|
+
for (let i = 0; i < objectDef.attributes.length; i++) {
|
|
2687
|
+
const attr = objectDef.attributes[i];
|
|
2688
|
+
const config = validatedConfigs[i];
|
|
2689
|
+
const dbAttr = await this.adapter.attributes.create({
|
|
2690
|
+
objectId: dbObject.id,
|
|
2691
|
+
name: attr.name,
|
|
2692
|
+
label: attr.label,
|
|
2693
|
+
type: attr.type,
|
|
2694
|
+
required: attr.required ?? false,
|
|
2695
|
+
unique: false,
|
|
2696
|
+
system: false,
|
|
2697
|
+
config
|
|
2698
|
+
});
|
|
2699
|
+
createdAttributes.push(this.convertDBAttributeToAttribute(dbAttr));
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
return {
|
|
2703
|
+
...objectDef,
|
|
2704
|
+
id: dbObject.id,
|
|
2705
|
+
attributes: createdAttributes,
|
|
2706
|
+
system: false
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Add a custom attribute to an existing object
|
|
2711
|
+
* Can be used on both custom and native objects
|
|
2712
|
+
* Attributes added via API are always custom (system=false)
|
|
2713
|
+
*
|
|
2714
|
+
* @param objectId - Object UUID from database
|
|
2715
|
+
* @param attribute - Attribute definition
|
|
2716
|
+
* @param tenantId - Tenant ID (required for relation validation)
|
|
2717
|
+
* @returns Created Attribute
|
|
2718
|
+
*/
|
|
2719
|
+
async addAttributeToObject(objectId, attribute, tenantId) {
|
|
2720
|
+
const dbObject = await this.adapter.objects.findById(objectId);
|
|
2721
|
+
if (!dbObject) {
|
|
2722
|
+
throw new Error(`Object with id "${objectId}" not found`);
|
|
2723
|
+
}
|
|
2724
|
+
const existingAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
2725
|
+
const duplicate = existingAttributes.find((attr) => attr.name === attribute.name);
|
|
2726
|
+
if (duplicate) {
|
|
2727
|
+
throw new Error(
|
|
2728
|
+
`Attribute with name "${attribute.name}" already exists on object "${dbObject.name}"`
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
const config = await this.validateAttributeInput(attribute, tenantId);
|
|
2732
|
+
const dbAttr = await this.adapter.attributes.create({
|
|
2733
|
+
objectId,
|
|
2734
|
+
name: attribute.name,
|
|
2735
|
+
label: attribute.label,
|
|
2736
|
+
type: attribute.type,
|
|
2737
|
+
required: attribute.required ?? false,
|
|
2738
|
+
unique: attribute.unique ?? false,
|
|
2739
|
+
system: false,
|
|
2740
|
+
// Always false for attributes added via API
|
|
2741
|
+
config
|
|
2742
|
+
});
|
|
2743
|
+
return this.convertDBAttributeToAttribute(dbAttr);
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Update an attribute
|
|
2747
|
+
* Can only update custom attributes (system=false)
|
|
2748
|
+
*
|
|
2749
|
+
* @param attributeId - Attribute UUID
|
|
2750
|
+
* @param updates - Partial attribute updates
|
|
2751
|
+
* @param tenantId - Tenant ID (required for relation validation)
|
|
2752
|
+
* @returns Updated Attribute
|
|
2753
|
+
*/
|
|
2754
|
+
async updateAttribute(attributeId, updates, tenantId) {
|
|
2755
|
+
const dbAttr = await this.adapter.attributes.findById(attributeId);
|
|
2756
|
+
if (!dbAttr) {
|
|
2757
|
+
throw new Error(`Attribute with id "${attributeId}" not found`);
|
|
2758
|
+
}
|
|
2759
|
+
if (dbAttr.system) {
|
|
2760
|
+
throw new Error(
|
|
2761
|
+
`Cannot modify system attribute "${dbAttr.name}". System attributes are protected.`
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
if (updates.name && updates.name !== dbAttr.name) {
|
|
2765
|
+
throw new Error(
|
|
2766
|
+
"Attribute name cannot be changed after creation. Create a new attribute instead."
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
if (updates.type && updates.type !== dbAttr.type) {
|
|
2770
|
+
throw new Error(
|
|
2771
|
+
"Attribute type cannot be changed after creation. Create a new attribute instead."
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2774
|
+
const mergedInput = {
|
|
2775
|
+
name: dbAttr.name,
|
|
2776
|
+
label: updates.label ?? dbAttr.label,
|
|
2777
|
+
type: dbAttr.type,
|
|
2778
|
+
required: updates.required ?? dbAttr.required,
|
|
2779
|
+
...dbAttr.config,
|
|
2780
|
+
...updates
|
|
2781
|
+
};
|
|
2782
|
+
const config = this.buildAttributeConfig(mergedInput);
|
|
2783
|
+
if (dbAttr.type === "relation" && updates.targets) {
|
|
2784
|
+
await this.validateRelationTargets(mergedInput, tenantId);
|
|
2785
|
+
}
|
|
2786
|
+
const updatedDbAttr = await this.adapter.attributes.update(attributeId, {
|
|
2787
|
+
label: updates.label,
|
|
2788
|
+
required: updates.required,
|
|
2789
|
+
config
|
|
2790
|
+
});
|
|
2791
|
+
return this.convertDBAttributeToAttribute(updatedDbAttr);
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* Delete an attribute
|
|
2795
|
+
* Can only delete custom attributes (system=false)
|
|
2796
|
+
*
|
|
2797
|
+
* @param attributeId - Attribute UUID
|
|
2798
|
+
*/
|
|
2799
|
+
async deleteAttribute(attributeId) {
|
|
2800
|
+
const dbAttr = await this.adapter.attributes.findById(attributeId);
|
|
2801
|
+
if (!dbAttr) {
|
|
2802
|
+
throw new Error(`Attribute with id "${attributeId}" not found`);
|
|
2803
|
+
}
|
|
2804
|
+
if (dbAttr.system) {
|
|
2805
|
+
throw new Error(
|
|
2806
|
+
`Cannot delete system attribute "${dbAttr.name}". System attributes are protected.`
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
await this.adapter.attributes.delete(attributeId);
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* List attributes for an object
|
|
2813
|
+
*
|
|
2814
|
+
* @param objectId - Object UUID
|
|
2815
|
+
* @param options - Filter options
|
|
2816
|
+
* @returns List of attributes
|
|
2817
|
+
*/
|
|
2818
|
+
async listAttributes(objectId, options) {
|
|
2819
|
+
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
2820
|
+
let filtered = dbAttributes;
|
|
2821
|
+
if (options?.systemOnly) {
|
|
2822
|
+
filtered = dbAttributes.filter((attr) => attr.system);
|
|
2823
|
+
} else if (options?.customOnly) {
|
|
2824
|
+
filtered = dbAttributes.filter((attr) => !attr.system);
|
|
2825
|
+
}
|
|
2826
|
+
return filtered.map((attr) => this.convertDBAttributeToAttribute(attr));
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Validate attribute name format (variable identifier)
|
|
2830
|
+
* @internal
|
|
2831
|
+
*/
|
|
2832
|
+
validateAttributeName(name) {
|
|
2833
|
+
const variableNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
2834
|
+
if (!name || name.length === 0) {
|
|
2835
|
+
throw new Error("Attribute name cannot be empty");
|
|
2836
|
+
}
|
|
2837
|
+
if (name.length > 63) {
|
|
2838
|
+
throw new Error("Attribute name is too long (max 63 characters)");
|
|
2839
|
+
}
|
|
2840
|
+
if (!variableNameRegex.test(name)) {
|
|
2841
|
+
throw new Error(
|
|
2842
|
+
"Invalid attribute name format.\nName must be a valid variable identifier:\n \u2705 Valid: 'firstName', 'first_name', 'FirstName', 'FIRST_NAME'\n \u274C Invalid: 'first-name', 'first name', '123name', 'first.name'"
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Get complete object schema (system + custom attributes)
|
|
2848
|
+
* If object is native, merges registry definition with DB custom attributes
|
|
2849
|
+
* If object is custom, returns DB definition only
|
|
2850
|
+
*
|
|
2851
|
+
* @param objectId - Object UUID from database
|
|
2852
|
+
* @returns Complete ObjectDefinition with all attributes
|
|
2853
|
+
*
|
|
2854
|
+
* @example
|
|
2855
|
+
* ```typescript
|
|
2856
|
+
* const service = new ObjectSchemaService(adapter, registry);
|
|
2857
|
+
* const productSchema = await service.getObjectSchema("obj-123");
|
|
2858
|
+
*
|
|
2859
|
+
* console.log(productSchema.attributes); // System + custom attributes
|
|
2860
|
+
* ```
|
|
2861
|
+
*/
|
|
2862
|
+
async getObjectSchema(objectId) {
|
|
2863
|
+
const dbObject = await this.adapter.objects.findById(objectId);
|
|
2864
|
+
if (!dbObject) {
|
|
2865
|
+
throw new Error(`Object with id "${objectId}" not found`);
|
|
2866
|
+
}
|
|
2867
|
+
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
2868
|
+
if (dbObject.system) {
|
|
2869
|
+
return this.mergeNativeObject(dbObject, dbAttributes);
|
|
2870
|
+
}
|
|
2871
|
+
return this.convertDBObjectToDefinition(dbObject, dbAttributes);
|
|
2872
|
+
}
|
|
2873
|
+
/**
|
|
2874
|
+
* Get object schema by name (for system/native objects)
|
|
2875
|
+
* @deprecated Use getObjectSchemaByNameForTenant for custom objects support
|
|
2876
|
+
*/
|
|
2877
|
+
async getObjectSchemaByName(name) {
|
|
2878
|
+
const dbObject = await this.adapter.objects.findSystemByName(name);
|
|
2879
|
+
if (!dbObject) {
|
|
2880
|
+
throw new Error(`Native object "${name}" not found in database`);
|
|
2881
|
+
}
|
|
2882
|
+
const dbAttributes = await this.adapter.attributes.findByObjectId(dbObject.id);
|
|
2883
|
+
return this.mergeNativeObject(dbObject, dbAttributes);
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* Get object schema by name for a tenant (supports both native and custom objects)
|
|
2887
|
+
*/
|
|
2888
|
+
async getObjectSchemaByNameForTenant(name, tenantId) {
|
|
2889
|
+
let dbObject = await this.adapter.objects.findByName(tenantId, name);
|
|
2890
|
+
if (!dbObject) {
|
|
2891
|
+
dbObject = await this.adapter.objects.findSystemByName(name);
|
|
2892
|
+
}
|
|
2893
|
+
if (!dbObject) {
|
|
2894
|
+
throw new Error(`Object "${name}" not found`);
|
|
2895
|
+
}
|
|
2896
|
+
const dbAttributes = await this.adapter.attributes.findByObjectId(dbObject.id);
|
|
2897
|
+
if (dbObject.system) {
|
|
2898
|
+
return this.mergeNativeObject(dbObject, dbAttributes);
|
|
2899
|
+
}
|
|
2900
|
+
return this.convertDBObjectToDefinition(dbObject, dbAttributes);
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* List all object schemas for a tenant
|
|
2904
|
+
*/
|
|
2905
|
+
async listObjectSchemas(tenantId) {
|
|
2906
|
+
const dbObjects = await this.adapter.objects.list(tenantId);
|
|
2907
|
+
const schemas = await Promise.all(
|
|
2908
|
+
dbObjects.map(async (dbObject) => {
|
|
2909
|
+
const dbAttributes = await this.adapter.attributes.findByObjectId(dbObject.id);
|
|
2910
|
+
if (dbObject.system) {
|
|
2911
|
+
return this.mergeNativeObject(dbObject, dbAttributes);
|
|
2912
|
+
}
|
|
2913
|
+
return this.convertDBObjectToDefinition(dbObject, dbAttributes);
|
|
2914
|
+
})
|
|
2915
|
+
);
|
|
2916
|
+
return schemas;
|
|
2917
|
+
}
|
|
2918
|
+
/**
|
|
2919
|
+
* Merge native object from registry with custom attributes from DB
|
|
2920
|
+
* @internal
|
|
2921
|
+
*/
|
|
2922
|
+
mergeNativeObject(dbObject, dbAttributes) {
|
|
2923
|
+
const nativeObject = this.nativeRegistry.getByName(dbObject.name);
|
|
2924
|
+
if (!nativeObject) {
|
|
2925
|
+
throw new Error(
|
|
2926
|
+
`Native object "${dbObject.name}" not found in registry. Make sure to register it before querying.`
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
const systemAttributesFromDB = dbAttributes.filter((attr) => attr.system);
|
|
2930
|
+
const customAttributes = dbAttributes.filter((attr) => !attr.system);
|
|
2931
|
+
const dbAttrByName = new Map(systemAttributesFromDB.map((attr) => [attr.name, attr]));
|
|
2932
|
+
const systemAttrs = nativeObject.attributes.map((registryAttr) => {
|
|
2933
|
+
const dbAttr = dbAttrByName.get(registryAttr.name);
|
|
2934
|
+
if (dbAttr) {
|
|
2935
|
+
return {
|
|
2936
|
+
...registryAttr,
|
|
2937
|
+
id: dbAttr.id
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
return registryAttr;
|
|
2941
|
+
});
|
|
2942
|
+
const customAttrs = customAttributes.map(
|
|
2943
|
+
(dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
|
|
2944
|
+
);
|
|
2945
|
+
return {
|
|
2946
|
+
...nativeObject,
|
|
2947
|
+
id: dbObject.id,
|
|
2948
|
+
attributes: [
|
|
2949
|
+
...systemAttrs,
|
|
2950
|
+
// System attributes from registry (with DB IDs if synced)
|
|
2951
|
+
...customAttrs
|
|
2952
|
+
// Custom attributes from DB
|
|
2953
|
+
]
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Convert DB object + attributes to ObjectDefinition
|
|
2958
|
+
* @internal
|
|
2959
|
+
*/
|
|
2960
|
+
convertDBObjectToDefinition(dbObject, dbAttributes) {
|
|
2961
|
+
const attributes = dbAttributes.map(
|
|
2962
|
+
(dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
|
|
2963
|
+
);
|
|
2964
|
+
return {
|
|
2965
|
+
id: dbObject.id,
|
|
2966
|
+
name: dbObject.name,
|
|
2967
|
+
label: dbObject.label,
|
|
2968
|
+
description: dbObject.description,
|
|
2969
|
+
icon: dbObject.metadata?.icon,
|
|
2970
|
+
labelExpression: dbObject.labelExpression,
|
|
2971
|
+
attributes,
|
|
2972
|
+
system: dbObject.system,
|
|
2973
|
+
metadata: dbObject.metadata
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Validate attribute input and return parsed config
|
|
2978
|
+
* Validates: name format, config schema, relation targets
|
|
2979
|
+
* @internal
|
|
2980
|
+
*/
|
|
2981
|
+
async validateAttributeInput(attribute, tenantId) {
|
|
2982
|
+
this.validateAttributeName(attribute.name);
|
|
2983
|
+
const config = this.buildAttributeConfig(attribute);
|
|
2984
|
+
if (attribute.type === "relation") {
|
|
2985
|
+
await this.validateRelationTargets(attribute, tenantId);
|
|
2986
|
+
}
|
|
2987
|
+
return config;
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Build and validate attribute config from input
|
|
2991
|
+
* Uses Zod schema to validate and strip unknown properties
|
|
2992
|
+
* @internal
|
|
2993
|
+
*/
|
|
2994
|
+
buildAttributeConfig(attribute) {
|
|
2995
|
+
const {
|
|
2996
|
+
name: _,
|
|
2997
|
+
label: __,
|
|
2998
|
+
type: ___,
|
|
2999
|
+
required: ____,
|
|
3000
|
+
unique: _____,
|
|
3001
|
+
...configInput
|
|
3002
|
+
} = attribute;
|
|
3003
|
+
try {
|
|
3004
|
+
return parseAttributeConfig(attribute.type, configInput);
|
|
3005
|
+
} catch (error) {
|
|
3006
|
+
if (error instanceof Error) {
|
|
3007
|
+
throw new Error(`Invalid config for ${attribute.type} attribute: ${error.message}`);
|
|
3008
|
+
}
|
|
3009
|
+
throw error;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Validate that relation target objects exist
|
|
3014
|
+
* @internal
|
|
3015
|
+
*/
|
|
3016
|
+
async validateRelationTargets(attribute, tenantId) {
|
|
3017
|
+
const targets = attribute.targets;
|
|
3018
|
+
if (!targets || targets.length === 0) {
|
|
3019
|
+
throw new Error("Relation attribute must have at least one target object");
|
|
3020
|
+
}
|
|
3021
|
+
const cardinality = attribute.cardinality;
|
|
3022
|
+
if (!(cardinality && ["one", "many"].includes(cardinality))) {
|
|
3023
|
+
throw new Error("Relation attribute must specify cardinality: 'one' or 'many'");
|
|
3024
|
+
}
|
|
3025
|
+
for (const target of targets) {
|
|
3026
|
+
if (!target.object) {
|
|
3027
|
+
throw new Error("Each relation target must specify an object name");
|
|
3028
|
+
}
|
|
3029
|
+
let targetObject = tenantId ? await this.adapter.objects.findByName(tenantId, target.object) : null;
|
|
3030
|
+
if (!targetObject) {
|
|
3031
|
+
targetObject = await this.adapter.objects.findSystemByName(target.object);
|
|
3032
|
+
}
|
|
3033
|
+
if (!targetObject) {
|
|
3034
|
+
const nativeObject = this.nativeRegistry.getByName(target.object);
|
|
3035
|
+
if (!nativeObject) {
|
|
3036
|
+
throw new Error(
|
|
3037
|
+
`Relation target object "${target.object}" does not exist. Make sure the object is created before adding a relation to it.`
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Convert DB attribute to Attribute type
|
|
3045
|
+
* @internal
|
|
3046
|
+
*/
|
|
3047
|
+
convertDBAttributeToAttribute(dbAttr) {
|
|
3048
|
+
const baseAttr = {
|
|
3049
|
+
...dbAttr.config,
|
|
3050
|
+
id: dbAttr.id,
|
|
3051
|
+
name: dbAttr.name,
|
|
3052
|
+
label: dbAttr.label,
|
|
3053
|
+
type: dbAttr.type,
|
|
3054
|
+
required: dbAttr.required,
|
|
3055
|
+
system: dbAttr.system,
|
|
3056
|
+
unique: dbAttr.unique
|
|
3057
|
+
};
|
|
3058
|
+
return baseAttr;
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3061
|
+
|
|
3062
|
+
// src/native/registry.ts
|
|
3063
|
+
var NativeObjectRegistryClass = class {
|
|
3064
|
+
constructor() {
|
|
3065
|
+
this.objects = /* @__PURE__ */ new Map();
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Register one or more native objects
|
|
3069
|
+
* Auto-builds if ObjectBuilder is passed instead of ObjectDefinition
|
|
3070
|
+
* @param objects - Single object/builder or array of objects/builders to register
|
|
3071
|
+
* @returns this (for chaining)
|
|
3072
|
+
* @throws Error if object is invalid or already registered
|
|
3073
|
+
*
|
|
3074
|
+
* @example
|
|
3075
|
+
* ```typescript
|
|
3076
|
+
* // No .build() needed - registry auto-builds!
|
|
3077
|
+
* const PRODUCT = object({ id: "obj-product", name: "products", label: "Product" })
|
|
3078
|
+
* .system()
|
|
3079
|
+
* .attribute(text({ id: "attr-name", name: "name", label: "Name" }));
|
|
3080
|
+
*
|
|
3081
|
+
* registry.register(PRODUCT); // ← Auto-builds here
|
|
3082
|
+
*
|
|
3083
|
+
* // Can still explicitly .build() if you want
|
|
3084
|
+
* registry.register(PRODUCT.build());
|
|
3085
|
+
*
|
|
3086
|
+
* // Register multiple
|
|
3087
|
+
* registry.register([PRODUCT, ORDER, CUSTOMER]);
|
|
3088
|
+
* ```
|
|
3089
|
+
*/
|
|
3090
|
+
register(objects) {
|
|
3091
|
+
const objectsArray = Array.isArray(objects) ? objects : [objects];
|
|
3092
|
+
for (const obj of objectsArray) {
|
|
3093
|
+
const object2 = "build" in obj && typeof obj.build === "function" ? obj.build() : obj;
|
|
3094
|
+
this.validateAndRegister(object2);
|
|
3095
|
+
}
|
|
3096
|
+
return this;
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Validate and register a single object
|
|
3100
|
+
* @internal
|
|
3101
|
+
*/
|
|
3102
|
+
validateAndRegister(object2) {
|
|
3103
|
+
if (!object2.system) {
|
|
3104
|
+
throw new Error(
|
|
3105
|
+
`[NativeObjectRegistry] Cannot register object "${object2.name}" (id: ${object2.id}): missing system flag.
|
|
3106
|
+
Native objects must have system=true. Did you forget to call .system() in your builder?`
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
if (this.objects.has(object2.name)) {
|
|
3110
|
+
const existing = this.objects.get(object2.name);
|
|
3111
|
+
throw new Error(
|
|
3112
|
+
`[NativeObjectRegistry] Duplicate object name "${object2.name}":
|
|
3113
|
+
- Existing: "${existing?.label}" (id: ${existing?.id})
|
|
3114
|
+
- New: "${object2.label}" (id: ${object2.id})
|
|
3115
|
+
Please use unique names for each native object.`
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
this.objects.set(object2.name, object2);
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Get a native object by its name
|
|
3122
|
+
* @param name - The object name
|
|
3123
|
+
* @returns The object definition or undefined if not found
|
|
3124
|
+
*
|
|
3125
|
+
* @example
|
|
3126
|
+
* ```typescript
|
|
3127
|
+
* const product = registry.getByName("products");
|
|
3128
|
+
* if (product) {
|
|
3129
|
+
* console.log(product.label); // "Product"
|
|
3130
|
+
* }
|
|
3131
|
+
* ```
|
|
3132
|
+
*/
|
|
3133
|
+
getByName(name) {
|
|
3134
|
+
return this.objects.get(name);
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Get a native object or throw if not found
|
|
3138
|
+
* @param name - The object name
|
|
3139
|
+
* @returns The object definition
|
|
3140
|
+
* @throws Error if not found
|
|
3141
|
+
*/
|
|
3142
|
+
getByNameOrThrow(name) {
|
|
3143
|
+
const object2 = this.objects.get(name);
|
|
3144
|
+
if (!object2) {
|
|
3145
|
+
throw new Error(
|
|
3146
|
+
`[NativeObjectRegistry] Native object "${name}" not found.
|
|
3147
|
+
Available objects: ${this.listNames().join(", ")}`
|
|
3148
|
+
);
|
|
3149
|
+
}
|
|
3150
|
+
return object2;
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Get all registered native objects
|
|
3154
|
+
* @returns Array of all registered object definitions
|
|
3155
|
+
*/
|
|
3156
|
+
getAll() {
|
|
3157
|
+
return Array.from(this.objects.values());
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Get all registered native object names
|
|
3161
|
+
* @returns Array of object names
|
|
3162
|
+
*/
|
|
3163
|
+
listNames() {
|
|
3164
|
+
return Array.from(this.objects.keys());
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Check if a native object is registered
|
|
3168
|
+
* @param name - The object name
|
|
3169
|
+
* @returns true if registered, false otherwise
|
|
3170
|
+
*/
|
|
3171
|
+
has(name) {
|
|
3172
|
+
return this.objects.has(name);
|
|
3173
|
+
}
|
|
3174
|
+
/**
|
|
3175
|
+
* Get the number of registered objects
|
|
3176
|
+
*/
|
|
3177
|
+
get size() {
|
|
3178
|
+
return this.objects.size;
|
|
3179
|
+
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Clear all registered objects (useful for testing)
|
|
3182
|
+
* @internal
|
|
3183
|
+
*/
|
|
3184
|
+
clear() {
|
|
3185
|
+
this.objects.clear();
|
|
3186
|
+
}
|
|
3187
|
+
/**
|
|
3188
|
+
* Get registry summary for debugging
|
|
3189
|
+
*/
|
|
3190
|
+
summary() {
|
|
3191
|
+
const objects = this.getAll();
|
|
3192
|
+
return [
|
|
3193
|
+
`[NativeObjectRegistry] ${this.size} native object(s) registered:`,
|
|
3194
|
+
...objects.map(
|
|
3195
|
+
(obj) => ` - ${obj.name}: "${obj.label}" (${obj.attributes.length} attributes)`
|
|
3196
|
+
)
|
|
3197
|
+
].join("\n");
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Log registry summary to console
|
|
3201
|
+
*/
|
|
3202
|
+
debug() {
|
|
3203
|
+
console.info(this.summary());
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
var registry = new NativeObjectRegistryClass();
|
|
3207
|
+
|
|
3208
|
+
// src/runtime/services/relation.service.ts
|
|
3209
|
+
var RelationService = class {
|
|
3210
|
+
constructor(adapter, nativeRegistry) {
|
|
3211
|
+
this.adapter = adapter;
|
|
3212
|
+
this.schemaService = new ObjectSchemaService(adapter, nativeRegistry);
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Validate all relation attributes in the data
|
|
3216
|
+
*
|
|
3217
|
+
* @param schema - Object schema containing attribute definitions
|
|
3218
|
+
* @param data - Record data to validate
|
|
3219
|
+
* @returns Validation result with errors if any
|
|
3220
|
+
*
|
|
3221
|
+
* @example
|
|
3222
|
+
* ```typescript
|
|
3223
|
+
* const result = await relationService.validateRelations(schema, {
|
|
3224
|
+
* company: "rec-123",
|
|
3225
|
+
* contacts: ["rec-456", "rec-789"]
|
|
3226
|
+
* });
|
|
3227
|
+
*
|
|
3228
|
+
* if (!result.valid) {
|
|
3229
|
+
* console.log(result.errors);
|
|
3230
|
+
* // [{ attribute: "company", message: "Record not found", invalidIds: ["rec-123"] }]
|
|
3231
|
+
* }
|
|
3232
|
+
* ```
|
|
3233
|
+
*/
|
|
3234
|
+
async validateRelations(schema, data) {
|
|
3235
|
+
const errors = [];
|
|
3236
|
+
const relationAttrs = schema.attributes.filter(
|
|
3237
|
+
(attr) => attr.type === "relation"
|
|
3238
|
+
);
|
|
3239
|
+
for (const attr of relationAttrs) {
|
|
3240
|
+
const value = data[attr.name];
|
|
3241
|
+
if (value === void 0 || value === null) {
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
const attrErrors = await this.validateRelationAttribute(attr, value);
|
|
3245
|
+
errors.push(...attrErrors);
|
|
3246
|
+
}
|
|
3247
|
+
return {
|
|
3248
|
+
valid: errors.length === 0,
|
|
3249
|
+
errors
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
/**
|
|
3253
|
+
* Validate a single relation attribute value
|
|
3254
|
+
*/
|
|
3255
|
+
async validateRelationAttribute(attr, value) {
|
|
3256
|
+
const errors = [];
|
|
3257
|
+
const ids = this.extractIds(attr, value);
|
|
3258
|
+
if (ids.length === 0) {
|
|
3259
|
+
return errors;
|
|
3260
|
+
}
|
|
3261
|
+
const validObjectIds = await this.getValidObjectIds(attr.targets);
|
|
3262
|
+
if (validObjectIds.size === 0) {
|
|
3263
|
+
errors.push({
|
|
3264
|
+
attribute: attr.name,
|
|
3265
|
+
message: `No valid target objects found for ${attr.label}`
|
|
3266
|
+
});
|
|
3267
|
+
return errors;
|
|
3268
|
+
}
|
|
3269
|
+
const invalidIds = [];
|
|
3270
|
+
for (const id of ids) {
|
|
3271
|
+
const record = await this.adapter.objectRecords.findById(id);
|
|
3272
|
+
if (!record) {
|
|
3273
|
+
invalidIds.push(id);
|
|
3274
|
+
continue;
|
|
3275
|
+
}
|
|
3276
|
+
if (!validObjectIds.has(record.objectId)) {
|
|
3277
|
+
invalidIds.push(id);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
if (invalidIds.length > 0) {
|
|
3281
|
+
errors.push({
|
|
3282
|
+
attribute: attr.name,
|
|
3283
|
+
message: `Invalid or non-existent records for ${attr.label}`,
|
|
3284
|
+
invalidIds
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
return errors;
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* Extract IDs from relation value based on cardinality
|
|
3291
|
+
*/
|
|
3292
|
+
extractIds(attr, value) {
|
|
3293
|
+
if (attr.cardinality === "many") {
|
|
3294
|
+
if (!Array.isArray(value)) {
|
|
3295
|
+
return [];
|
|
3296
|
+
}
|
|
3297
|
+
return value.filter((v) => typeof v === "string");
|
|
3298
|
+
}
|
|
3299
|
+
if (typeof value === "string") {
|
|
3300
|
+
return [value];
|
|
3301
|
+
}
|
|
3302
|
+
return [];
|
|
3303
|
+
}
|
|
3304
|
+
/**
|
|
3305
|
+
* Get valid object IDs from relation targets
|
|
3306
|
+
*/
|
|
3307
|
+
async getValidObjectIds(targets) {
|
|
3308
|
+
const objectIds = /* @__PURE__ */ new Set();
|
|
3309
|
+
for (const target of targets) {
|
|
3310
|
+
try {
|
|
3311
|
+
const objectSchema = await this.schemaService.getObjectSchemaByName(target.object);
|
|
3312
|
+
if (objectSchema?.id) {
|
|
3313
|
+
objectIds.add(objectSchema.id);
|
|
3314
|
+
}
|
|
3315
|
+
} catch {
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
return objectIds;
|
|
3319
|
+
}
|
|
3320
|
+
/**
|
|
3321
|
+
* Validate relations and throw if invalid
|
|
3322
|
+
*/
|
|
3323
|
+
async validateRelationsOrThrow(schema, data) {
|
|
3324
|
+
const result = await this.validateRelations(schema, data);
|
|
3325
|
+
if (!result.valid) {
|
|
3326
|
+
const messages = result.errors.map((e) => `${e.attribute}: ${e.message}`).join("; ");
|
|
3327
|
+
throw new Error(`Relation validation failed: ${messages}`);
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
// ============================================================================
|
|
3331
|
+
// OPTIONS & RESOLUTION
|
|
3332
|
+
// ============================================================================
|
|
3333
|
+
/**
|
|
3334
|
+
* Get available options for a relation attribute
|
|
3335
|
+
* Searches across all target objects defined in the relation
|
|
3336
|
+
*
|
|
3337
|
+
* @param attribute - Relation attribute definition
|
|
3338
|
+
* @param tenantId - Tenant ID for multi-tenant isolation
|
|
3339
|
+
* @param params - Query parameters
|
|
3340
|
+
*
|
|
3341
|
+
* @example
|
|
3342
|
+
* ```typescript
|
|
3343
|
+
* const options = await relationService.getOptions(attribute, "tenant-1", {
|
|
3344
|
+
* query: "nike",
|
|
3345
|
+
* page: 1,
|
|
3346
|
+
* pageSize: 20
|
|
3347
|
+
* });
|
|
3348
|
+
* ```
|
|
3349
|
+
*/
|
|
3350
|
+
async getOptions(attribute, tenantId, params = {}) {
|
|
3351
|
+
const { query = "", page = 1, pageSize = 20, targetObject } = params;
|
|
3352
|
+
const targets = attribute.targets;
|
|
3353
|
+
const filteredTargets = targetObject ? targets.filter((t) => t.object === targetObject) : targets;
|
|
3354
|
+
if (filteredTargets.length === 0) {
|
|
3355
|
+
return { options: [], hasMore: false, total: 0 };
|
|
3356
|
+
}
|
|
3357
|
+
const allOptions = [];
|
|
3358
|
+
let totalCount = 0;
|
|
3359
|
+
const recordService = new RecordService(this.adapter, tenantId);
|
|
3360
|
+
for (const target of filteredTargets) {
|
|
3361
|
+
const objectSchema = await this.schemaService.getObjectSchemaByName(target.object);
|
|
3362
|
+
if (!objectSchema?.id) {
|
|
3363
|
+
continue;
|
|
3364
|
+
}
|
|
3365
|
+
const result = query ? await recordService.searchRecords(objectSchema.id, query, {
|
|
3366
|
+
limit: pageSize,
|
|
3367
|
+
offset: (page - 1) * pageSize
|
|
3368
|
+
}) : await recordService.listRecords(objectSchema.id, {
|
|
3369
|
+
limit: pageSize,
|
|
3370
|
+
offset: (page - 1) * pageSize
|
|
3371
|
+
});
|
|
3372
|
+
totalCount += result.total;
|
|
3373
|
+
for (const record of result.records) {
|
|
3374
|
+
const template = objectSchema.labelExpression;
|
|
3375
|
+
const label = renderLabelExpression(template, record.values);
|
|
3376
|
+
allOptions.push({
|
|
3377
|
+
id: record.id,
|
|
3378
|
+
objectId: objectSchema.id,
|
|
3379
|
+
objectName: objectSchema.name,
|
|
3380
|
+
objectLabel: objectSchema.label,
|
|
3381
|
+
objectIcon: objectSchema.icon,
|
|
3382
|
+
label,
|
|
3383
|
+
data: record.values
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
const hasMore = totalCount > page * pageSize;
|
|
3388
|
+
return {
|
|
3389
|
+
options: allOptions.slice(0, pageSize),
|
|
3390
|
+
hasMore,
|
|
3391
|
+
total: totalCount
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* Resolve record IDs to their display labels
|
|
3396
|
+
* Useful for displaying current values in the UI
|
|
3397
|
+
*
|
|
3398
|
+
* @param ids - Record IDs to resolve
|
|
3399
|
+
* @param tenantId - Tenant ID for multi-tenant isolation
|
|
3400
|
+
*
|
|
3401
|
+
* @example
|
|
3402
|
+
* ```typescript
|
|
3403
|
+
* const resolved = await relationService.resolveIds(["rec-1", "rec-2"], "tenant-1");
|
|
3404
|
+
* // [{ id: "rec-1", label: "Nike Air Max", objectName: "products", ... }]
|
|
3405
|
+
* ```
|
|
3406
|
+
*/
|
|
3407
|
+
async resolveIds(ids, tenantId) {
|
|
3408
|
+
if (!ids || ids.length === 0) {
|
|
3409
|
+
return [];
|
|
3410
|
+
}
|
|
3411
|
+
const recordService = new RecordService(this.adapter, tenantId);
|
|
3412
|
+
const resolved = [];
|
|
3413
|
+
for (const id of ids) {
|
|
3414
|
+
const record = await recordService.getRecord(id);
|
|
3415
|
+
if (!record) {
|
|
3416
|
+
continue;
|
|
3417
|
+
}
|
|
3418
|
+
const objectSchema = await this.schemaService.getObjectSchema(record.objectId);
|
|
3419
|
+
if (!objectSchema) {
|
|
3420
|
+
continue;
|
|
3421
|
+
}
|
|
3422
|
+
const label = renderLabelExpression(objectSchema.labelExpression, record.values);
|
|
3423
|
+
resolved.push({
|
|
3424
|
+
id: record.id,
|
|
3425
|
+
objectId: record.objectId,
|
|
3426
|
+
objectName: objectSchema.name,
|
|
3427
|
+
objectLabel: objectSchema.label,
|
|
3428
|
+
objectIcon: objectSchema.icon,
|
|
3429
|
+
label,
|
|
3430
|
+
data: record.values
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
return resolved;
|
|
3434
|
+
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Find a relation attribute by ID
|
|
3437
|
+
*/
|
|
3438
|
+
async findAttributeById(attributeId) {
|
|
3439
|
+
const attribute = await this.adapter.attributes.findById(attributeId);
|
|
3440
|
+
if (!attribute || attribute.type !== "relation") {
|
|
3441
|
+
return null;
|
|
3442
|
+
}
|
|
3443
|
+
return {
|
|
3444
|
+
...attribute.config,
|
|
3445
|
+
...attribute
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
};
|
|
3449
|
+
|
|
3450
|
+
// src/runtime/services/record.service.ts
|
|
3451
|
+
var RecordService = class {
|
|
3452
|
+
constructor(adapter, tenantId, options) {
|
|
3453
|
+
this.adapter = adapter;
|
|
3454
|
+
this.tenantId = tenantId;
|
|
3455
|
+
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
3456
|
+
this.relationService = new RelationService(adapter, registry);
|
|
3457
|
+
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Compute display label from schema expression
|
|
3461
|
+
* @internal
|
|
3462
|
+
*/
|
|
3463
|
+
computeLabel(schema, values) {
|
|
3464
|
+
return renderLabelExpression(schema.labelExpression, values);
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Create a new record with validation
|
|
3468
|
+
*
|
|
3469
|
+
* Triggers beforeCreate and afterCreate hooks if a HookRegistry is configured.
|
|
3470
|
+
*
|
|
3471
|
+
* @param objectId - Object UUID
|
|
3472
|
+
* @param data - Record data (attribute values)
|
|
3473
|
+
* @param options - Creation options
|
|
3474
|
+
* @returns Created record with computed completionStatus
|
|
3475
|
+
*
|
|
3476
|
+
* @example
|
|
3477
|
+
* ```typescript
|
|
3478
|
+
* const service = new RecordService(adapter, "tenant-123");
|
|
3479
|
+
*
|
|
3480
|
+
* // Create a complete record (strict validation)
|
|
3481
|
+
* const product = await service.createRecord("obj-product", {
|
|
3482
|
+
* name: "Nike Air Max",
|
|
3483
|
+
* price: 129.99,
|
|
3484
|
+
* status: "active"
|
|
3485
|
+
* });
|
|
3486
|
+
* // → product.completionStatus = "complete"
|
|
3487
|
+
*
|
|
3488
|
+
* // Create a draft record (allows missing required fields)
|
|
3489
|
+
* const draft = await service.createRecord("obj-product", {
|
|
3490
|
+
* name: "Draft Product"
|
|
3491
|
+
* }, { allowDraft: true });
|
|
3492
|
+
* // → draft.completionStatus = "draft"
|
|
3493
|
+
* ```
|
|
3494
|
+
*/
|
|
3495
|
+
async createRecord(objectId, data, options) {
|
|
3496
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
3497
|
+
const hookCtx = this.buildCreateHookContext(schema, data, options?.hookMetadata);
|
|
3498
|
+
if (!options?.skipHooks) {
|
|
3499
|
+
await this.hookRegistry.execute("beforeCreate", schema.name, hookCtx);
|
|
3500
|
+
}
|
|
3501
|
+
if (options?.validate !== false) {
|
|
3502
|
+
if (options?.allowDraft) {
|
|
3503
|
+
validateDraftOrThrow(schema, data);
|
|
3504
|
+
} else {
|
|
3505
|
+
validateObjectOrThrow(schema, data);
|
|
3506
|
+
}
|
|
3507
|
+
if (!options?.skipRelationValidation) {
|
|
3508
|
+
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
const completionStatus = computeRecordStatus(schema, data);
|
|
3512
|
+
const label = this.computeLabel(schema, data);
|
|
3513
|
+
const record = await this.adapter.objectRecords.create({
|
|
3514
|
+
tenantId: this.tenantId,
|
|
3515
|
+
objectId,
|
|
3516
|
+
data,
|
|
3517
|
+
label,
|
|
3518
|
+
completionStatus
|
|
3519
|
+
});
|
|
3520
|
+
if (!options?.skipHooks) {
|
|
3521
|
+
const afterCtx = {
|
|
3522
|
+
...hookCtx,
|
|
3523
|
+
recordId: record.id,
|
|
3524
|
+
record
|
|
3525
|
+
};
|
|
3526
|
+
await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
|
|
3527
|
+
}
|
|
3528
|
+
return record;
|
|
3529
|
+
}
|
|
3530
|
+
/**
|
|
3531
|
+
* Get a record by ID
|
|
3532
|
+
*
|
|
3533
|
+
* @param recordId - Record UUID
|
|
3534
|
+
* @param options - Query options
|
|
3535
|
+
* @returns Record or null if not found
|
|
3536
|
+
*/
|
|
3537
|
+
async getRecord(recordId, options) {
|
|
3538
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
3539
|
+
if (!record) {
|
|
3540
|
+
return null;
|
|
3541
|
+
}
|
|
3542
|
+
if (options?.includeSchema) {
|
|
3543
|
+
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
3544
|
+
const recordWithSchema = record;
|
|
3545
|
+
recordWithSchema.schema = schema;
|
|
3546
|
+
return recordWithSchema;
|
|
3547
|
+
}
|
|
3548
|
+
return record;
|
|
3549
|
+
}
|
|
3550
|
+
/**
|
|
3551
|
+
* Get a record by ID or throw if not found
|
|
3552
|
+
*/
|
|
3553
|
+
async getRecordOrThrow(recordId) {
|
|
3554
|
+
const record = await this.getRecord(recordId);
|
|
3555
|
+
if (!record) {
|
|
3556
|
+
throw new Error(`Record with id "${recordId}" not found`);
|
|
3557
|
+
}
|
|
3558
|
+
return record;
|
|
3559
|
+
}
|
|
3560
|
+
/**
|
|
3561
|
+
* Update a record with validation
|
|
3562
|
+
*
|
|
3563
|
+
* The completion status is automatically recalculated after each update.
|
|
3564
|
+
* A draft record becomes complete when all required fields are filled.
|
|
3565
|
+
*
|
|
3566
|
+
* Triggers beforeUpdate and afterUpdate hooks if a HookRegistry is configured.
|
|
3567
|
+
*
|
|
3568
|
+
* @param recordId - Record UUID
|
|
3569
|
+
* @param data - Partial data to update
|
|
3570
|
+
* @param options - Update options
|
|
3571
|
+
* @returns Updated record with recalculated completionStatus
|
|
3572
|
+
*
|
|
3573
|
+
* @example
|
|
3574
|
+
* ```typescript
|
|
3575
|
+
* // Update a draft record to make it complete
|
|
3576
|
+
* const updated = await service.updateRecord(draftId, {
|
|
3577
|
+
* price: 99.99,
|
|
3578
|
+
* status: "active"
|
|
3579
|
+
* });
|
|
3580
|
+
* // → updated.completionStatus = "complete" if all required fields now present
|
|
3581
|
+
* ```
|
|
3582
|
+
*/
|
|
3583
|
+
async updateRecord(recordId, data, options) {
|
|
3584
|
+
const existing = await this.getRecordOrThrow(recordId);
|
|
3585
|
+
const schema = await this.schemaService.getObjectSchema(existing.objectId);
|
|
3586
|
+
const mergedData = { ...existing.values, ...data };
|
|
3587
|
+
const changedAttributes = Object.keys(data).filter((key) => existing.values[key] !== data[key]);
|
|
3588
|
+
const hookCtx = this.buildHookContext(
|
|
3589
|
+
schema,
|
|
3590
|
+
existing,
|
|
3591
|
+
data,
|
|
3592
|
+
mergedData,
|
|
3593
|
+
changedAttributes,
|
|
3594
|
+
options?.hookMetadata
|
|
3595
|
+
);
|
|
3596
|
+
if (!options?.skipHooks) {
|
|
3597
|
+
await this.hookRegistry.execute("beforeUpdate", schema.name, hookCtx);
|
|
3598
|
+
}
|
|
3599
|
+
if (options?.validate !== false) {
|
|
3600
|
+
if (options?.partial) {
|
|
3601
|
+
validateDraftOrThrow(schema, mergedData);
|
|
3602
|
+
} else {
|
|
3603
|
+
validateObjectOrThrow(schema, mergedData);
|
|
3604
|
+
}
|
|
3605
|
+
if (!options?.skipRelationValidation) {
|
|
3606
|
+
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
3610
|
+
const label = this.computeLabel(schema, mergedData);
|
|
3611
|
+
const updated = await this.adapter.objectRecords.update(recordId, {
|
|
3612
|
+
...data,
|
|
3613
|
+
__completionStatus: completionStatus,
|
|
3614
|
+
__label: label
|
|
3615
|
+
});
|
|
3616
|
+
if (!options?.skipHooks) {
|
|
3617
|
+
const afterCtx = {
|
|
3618
|
+
...hookCtx,
|
|
3619
|
+
record: updated
|
|
3620
|
+
};
|
|
3621
|
+
await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
|
|
3622
|
+
}
|
|
3623
|
+
return updated;
|
|
3624
|
+
}
|
|
3625
|
+
/**
|
|
3626
|
+
* Build hook context for update operations
|
|
3627
|
+
* @internal
|
|
3628
|
+
*/
|
|
3629
|
+
buildHookContext(schema, existing, newData, mergedData, changedAttributes, metadata) {
|
|
3630
|
+
const oldValuesSnapshot = { ...existing.values };
|
|
3631
|
+
return {
|
|
3632
|
+
objectId: schema.id,
|
|
3633
|
+
objectName: schema.name,
|
|
3634
|
+
recordId: existing.id,
|
|
3635
|
+
tenantId: this.tenantId,
|
|
3636
|
+
record: existing,
|
|
3637
|
+
oldValues: oldValuesSnapshot,
|
|
3638
|
+
newValues: mergedData,
|
|
3639
|
+
changedAttributes,
|
|
3640
|
+
getChange: (attr) => ({
|
|
3641
|
+
oldValue: oldValuesSnapshot[attr],
|
|
3642
|
+
newValue: newData[attr] ?? oldValuesSnapshot[attr],
|
|
3643
|
+
changed: changedAttributes.includes(attr)
|
|
3644
|
+
}),
|
|
3645
|
+
metadata: metadata ?? {},
|
|
3646
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Build hook context for create operations (no existing record)
|
|
3651
|
+
* @internal
|
|
3652
|
+
*/
|
|
3653
|
+
buildCreateHookContext(schema, data, metadata) {
|
|
3654
|
+
const attributeNames = Object.keys(data);
|
|
3655
|
+
return {
|
|
3656
|
+
objectId: schema.id,
|
|
3657
|
+
objectName: schema.name,
|
|
3658
|
+
recordId: "",
|
|
3659
|
+
// Will be set after creation
|
|
3660
|
+
tenantId: this.tenantId,
|
|
3661
|
+
record: null,
|
|
3662
|
+
// Will be set after creation
|
|
3663
|
+
oldValues: {},
|
|
3664
|
+
newValues: data,
|
|
3665
|
+
changedAttributes: attributeNames,
|
|
3666
|
+
// All attributes are "new"
|
|
3667
|
+
getChange: (attr) => ({
|
|
3668
|
+
oldValue: void 0,
|
|
3669
|
+
newValue: data[attr],
|
|
3670
|
+
changed: attributeNames.includes(attr)
|
|
3671
|
+
}),
|
|
3672
|
+
metadata: metadata ?? {},
|
|
3673
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
/**
|
|
3677
|
+
* Build hook context for delete operations
|
|
3678
|
+
* @internal
|
|
3679
|
+
*/
|
|
3680
|
+
buildDeleteHookContext(schema, record, metadata) {
|
|
3681
|
+
const attributeNames = Object.keys(record.values);
|
|
3682
|
+
return {
|
|
3683
|
+
objectId: schema.id,
|
|
3684
|
+
objectName: schema.name,
|
|
3685
|
+
recordId: record.id,
|
|
3686
|
+
tenantId: this.tenantId,
|
|
3687
|
+
record,
|
|
3688
|
+
oldValues: record.values,
|
|
3689
|
+
newValues: {},
|
|
3690
|
+
changedAttributes: attributeNames,
|
|
3691
|
+
// All attributes are being "removed"
|
|
3692
|
+
getChange: (attr) => ({
|
|
3693
|
+
oldValue: record.values[attr],
|
|
3694
|
+
newValue: void 0,
|
|
3695
|
+
changed: attributeNames.includes(attr)
|
|
3696
|
+
}),
|
|
3697
|
+
metadata: metadata ?? {},
|
|
3698
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
/**
|
|
3702
|
+
* Delete a record
|
|
3703
|
+
*
|
|
3704
|
+
* Triggers beforeDelete and afterDelete hooks if a HookRegistry is configured.
|
|
3705
|
+
*
|
|
3706
|
+
* @param recordId - Record UUID
|
|
3707
|
+
* @param options - Delete options
|
|
3708
|
+
*/
|
|
3709
|
+
async deleteRecord(recordId, options) {
|
|
3710
|
+
const record = await this.getRecordOrThrow(recordId);
|
|
3711
|
+
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
3712
|
+
if (options?.checkSystem) {
|
|
3713
|
+
if (schema.system) {
|
|
3714
|
+
throw new Error(`Cannot delete record of system object "${schema.label}"`);
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
const hookCtx = this.buildDeleteHookContext(schema, record, options?.hookMetadata);
|
|
3718
|
+
if (!options?.skipHooks) {
|
|
3719
|
+
await this.hookRegistry.execute("beforeDelete", schema.name, hookCtx);
|
|
3720
|
+
}
|
|
3721
|
+
await this.adapter.objectRecords.delete(recordId);
|
|
3722
|
+
if (!options?.skipHooks) {
|
|
3723
|
+
await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
/**
|
|
3727
|
+
* List records for an object with pagination
|
|
3728
|
+
*
|
|
3729
|
+
* @param objectId - Object UUID
|
|
3730
|
+
* @param options - List options
|
|
3731
|
+
* @returns Records and total count
|
|
3732
|
+
*/
|
|
3733
|
+
async listRecords(objectId, options) {
|
|
3734
|
+
return await this.adapter.objectRecords.list(this.tenantId, objectId, options);
|
|
3735
|
+
}
|
|
3736
|
+
/**
|
|
3737
|
+
* Search records using full-text search
|
|
3738
|
+
*
|
|
3739
|
+
* @param objectId - Object UUID
|
|
3740
|
+
* @param query - Search query
|
|
3741
|
+
* @param options - Search options
|
|
3742
|
+
* @returns Matching records and total count
|
|
3743
|
+
*/
|
|
3744
|
+
async searchRecords(objectId, query, options) {
|
|
3745
|
+
return await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
|
|
3746
|
+
}
|
|
3747
|
+
/**
|
|
3748
|
+
* Validate data against object schema without saving
|
|
3749
|
+
*
|
|
3750
|
+
* @param objectId - Object UUID
|
|
3751
|
+
* @param data - Data to validate
|
|
3752
|
+
* @returns Validation result
|
|
3753
|
+
*/
|
|
3754
|
+
async validateData(objectId, data) {
|
|
3755
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
3756
|
+
return validateObject(schema, data);
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Compute the completion status for given data without saving.
|
|
3760
|
+
* Useful for UI to show draft/complete status before submitting.
|
|
3761
|
+
*
|
|
3762
|
+
* @param objectId - Object UUID
|
|
3763
|
+
* @param data - Data to check
|
|
3764
|
+
* @returns Computed completion status
|
|
3765
|
+
*/
|
|
3766
|
+
async computeStatus(objectId, data) {
|
|
3767
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
3768
|
+
return computeRecordStatus(schema, data);
|
|
3769
|
+
}
|
|
3770
|
+
/**
|
|
3771
|
+
* Refresh the completion status of an existing record.
|
|
3772
|
+
* Useful when schema changes and you need to recompute statuses.
|
|
3773
|
+
*
|
|
3774
|
+
* @param recordId - Record UUID
|
|
3775
|
+
* @returns Updated completion status
|
|
3776
|
+
*/
|
|
3777
|
+
async refreshRecordStatus(recordId) {
|
|
3778
|
+
const record = await this.getRecordOrThrow(recordId);
|
|
3779
|
+
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
3780
|
+
const newStatus = computeRecordStatus(schema, record.values);
|
|
3781
|
+
if (record.completionStatus !== newStatus) {
|
|
3782
|
+
await this.adapter.objectRecords.update(recordId, {
|
|
3783
|
+
__completionStatus: newStatus
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
return newStatus;
|
|
3787
|
+
}
|
|
3788
|
+
};
|
|
3789
|
+
|
|
3790
|
+
// src/runtime/services/user-profile.service.ts
|
|
3791
|
+
var UserProfileService = class {
|
|
3792
|
+
constructor(adapter, tenantId) {
|
|
3793
|
+
this.adapter = adapter;
|
|
3794
|
+
this.tenantId = tenantId;
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Create a new user profile (typically after first auth)
|
|
3798
|
+
*
|
|
3799
|
+
* @param data - User profile creation data
|
|
3800
|
+
* @returns Created user profile
|
|
3801
|
+
*
|
|
3802
|
+
* @example
|
|
3803
|
+
* ```typescript
|
|
3804
|
+
* const service = new UserProfileService(adapter, "tenant-123");
|
|
3805
|
+
*
|
|
3806
|
+
* // After Supabase auth
|
|
3807
|
+
* const profile = await service.createProfile({
|
|
3808
|
+
* tenantId: "tenant-123",
|
|
3809
|
+
* authId: authUser.id,
|
|
3810
|
+
* email: authUser.email,
|
|
3811
|
+
* firstName: authUser.user_metadata.first_name,
|
|
3812
|
+
* lastName: authUser.user_metadata.last_name,
|
|
3813
|
+
* role: "member",
|
|
3814
|
+
* status: "active"
|
|
3815
|
+
* });
|
|
3816
|
+
* ```
|
|
3817
|
+
*/
|
|
3818
|
+
async createProfile(data) {
|
|
3819
|
+
if (data.tenantId !== this.tenantId) {
|
|
3820
|
+
throw new Error(
|
|
3821
|
+
`Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
|
|
3822
|
+
);
|
|
3823
|
+
}
|
|
3824
|
+
const existing = await this.adapter.userProfiles.findByAuthId(data.authId);
|
|
3825
|
+
if (existing) {
|
|
3826
|
+
throw new Error(`User profile with authId "${data.authId}" already exists`);
|
|
3827
|
+
}
|
|
3828
|
+
const existingEmail = await this.adapter.userProfiles.findByEmail(this.tenantId, data.email);
|
|
3829
|
+
if (existingEmail) {
|
|
3830
|
+
throw new Error(`User profile with email "${data.email}" already exists in this tenant`);
|
|
3831
|
+
}
|
|
3832
|
+
return await this.adapter.userProfiles.create(data);
|
|
3833
|
+
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Get user profile by ID
|
|
3836
|
+
*/
|
|
3837
|
+
async getProfile(profileId) {
|
|
3838
|
+
return await this.adapter.userProfiles.findById(profileId);
|
|
3839
|
+
}
|
|
3840
|
+
/**
|
|
3841
|
+
* Get user profile by ID or throw
|
|
3842
|
+
*/
|
|
3843
|
+
async getProfileOrThrow(profileId) {
|
|
3844
|
+
const profile = await this.getProfile(profileId);
|
|
3845
|
+
if (!profile) {
|
|
3846
|
+
throw new Error(`User profile with id "${profileId}" not found`);
|
|
3847
|
+
}
|
|
3848
|
+
return profile;
|
|
3849
|
+
}
|
|
3850
|
+
/**
|
|
3851
|
+
* Get user profile by auth provider ID
|
|
3852
|
+
*
|
|
3853
|
+
* @param authId - Auth provider user ID (Supabase, Clerk, etc.)
|
|
3854
|
+
* @returns User profile or null
|
|
3855
|
+
*/
|
|
3856
|
+
async getProfileByAuthId(authId) {
|
|
3857
|
+
return await this.adapter.userProfiles.findByAuthId(authId);
|
|
3858
|
+
}
|
|
3859
|
+
/**
|
|
3860
|
+
* Get or create user profile (idempotent operation)
|
|
3861
|
+
* Useful for auth callbacks - ensures profile exists
|
|
3862
|
+
*
|
|
3863
|
+
* @param authId - Auth provider user ID
|
|
3864
|
+
* @param data - Profile data to create if doesn't exist
|
|
3865
|
+
* @returns Existing or newly created profile
|
|
3866
|
+
*
|
|
3867
|
+
* @example
|
|
3868
|
+
* ```typescript
|
|
3869
|
+
* // In Supabase auth callback
|
|
3870
|
+
* const profile = await service.getOrCreateProfile(
|
|
3871
|
+
* authUser.id,
|
|
3872
|
+
* {
|
|
3873
|
+
* tenantId: "tenant-123",
|
|
3874
|
+
* authId: authUser.id,
|
|
3875
|
+
* email: authUser.email,
|
|
3876
|
+
* role: "member",
|
|
3877
|
+
* status: "active"
|
|
3878
|
+
* }
|
|
3879
|
+
* );
|
|
3880
|
+
* ```
|
|
3881
|
+
*/
|
|
3882
|
+
async getOrCreateProfile(authId, data) {
|
|
3883
|
+
const existing = await this.getProfileByAuthId(authId);
|
|
3884
|
+
if (existing) {
|
|
3885
|
+
return existing;
|
|
3886
|
+
}
|
|
3887
|
+
return await this.createProfile(data);
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* Update user profile
|
|
3891
|
+
*/
|
|
3892
|
+
async updateProfile(profileId, data) {
|
|
3893
|
+
await this.getProfileOrThrow(profileId);
|
|
3894
|
+
return await this.adapter.userProfiles.update(profileId, data);
|
|
3895
|
+
}
|
|
3896
|
+
/**
|
|
3897
|
+
* Delete user profile
|
|
3898
|
+
*
|
|
3899
|
+
* @param profileId - Profile UUID
|
|
3900
|
+
* @param options - Delete options
|
|
3901
|
+
*/
|
|
3902
|
+
async deleteProfile(profileId, options) {
|
|
3903
|
+
if (options?.checkAdmin) {
|
|
3904
|
+
const profile = await this.getProfileOrThrow(profileId);
|
|
3905
|
+
if (profile.role === "admin") {
|
|
3906
|
+
const allProfiles = await this.listProfiles();
|
|
3907
|
+
const adminCount = allProfiles.filter((p) => p.role === "admin").length;
|
|
3908
|
+
if (adminCount <= 1) {
|
|
3909
|
+
throw new Error("Cannot delete the last admin user");
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
await this.adapter.userProfiles.delete(profileId);
|
|
3914
|
+
}
|
|
3915
|
+
/**
|
|
3916
|
+
* List all user profiles for the tenant
|
|
3917
|
+
*/
|
|
3918
|
+
async listProfiles(options) {
|
|
3919
|
+
return await this.adapter.userProfiles.list(this.tenantId, options);
|
|
3920
|
+
}
|
|
3921
|
+
/**
|
|
3922
|
+
* Update last login timestamp
|
|
3923
|
+
*
|
|
3924
|
+
* @param profileId - Profile UUID
|
|
3925
|
+
*
|
|
3926
|
+
* @example
|
|
3927
|
+
* ```typescript
|
|
3928
|
+
* // After successful auth
|
|
3929
|
+
* await service.updateLastLogin(profile.id);
|
|
3930
|
+
* ```
|
|
3931
|
+
*/
|
|
3932
|
+
async updateLastLogin(profileId) {
|
|
3933
|
+
await this.adapter.userProfiles.updateLastLogin(profileId);
|
|
3934
|
+
}
|
|
3935
|
+
/**
|
|
3936
|
+
* Change user role
|
|
3937
|
+
*
|
|
3938
|
+
* @param profileId - Profile UUID
|
|
3939
|
+
* @param newRole - New role
|
|
3940
|
+
*/
|
|
3941
|
+
async changeRole(profileId, newRole) {
|
|
3942
|
+
return await this.updateProfile(profileId, { role: newRole });
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Change user status
|
|
3946
|
+
*
|
|
3947
|
+
* @param profileId - Profile UUID
|
|
3948
|
+
* @param newStatus - New status
|
|
3949
|
+
*/
|
|
3950
|
+
async changeStatus(profileId, newStatus) {
|
|
3951
|
+
return await this.updateProfile(profileId, { status: newStatus });
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Get user by email
|
|
3955
|
+
*/
|
|
3956
|
+
async getProfileByEmail(email) {
|
|
3957
|
+
return await this.adapter.userProfiles.findByEmail(this.tenantId, email);
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* Check if user has role
|
|
3961
|
+
*/
|
|
3962
|
+
async hasRole(profileId, role) {
|
|
3963
|
+
const profile = await this.getProfile(profileId);
|
|
3964
|
+
return profile?.role === role;
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Check if user is admin
|
|
3968
|
+
*/
|
|
3969
|
+
async isAdmin(profileId) {
|
|
3970
|
+
return await this.hasRole(profileId, "admin");
|
|
3971
|
+
}
|
|
3972
|
+
};
|
|
3973
|
+
|
|
3974
|
+
// src/runtime/services/view.service.ts
|
|
3975
|
+
var ViewService = class {
|
|
3976
|
+
constructor(adapter, nativeViews) {
|
|
3977
|
+
this.adapter = adapter;
|
|
3978
|
+
this.nativeViews = nativeViews;
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Get all views for an object (native + custom)
|
|
3982
|
+
*
|
|
3983
|
+
* @param objectName - Object name
|
|
3984
|
+
* @param tenantId - Tenant ID
|
|
3985
|
+
* @returns All views for the object
|
|
3986
|
+
*/
|
|
3987
|
+
async getViewsForObject(objectName, tenantId) {
|
|
3988
|
+
const nativeViewDefs = this.nativeViews.getByObjectName(objectName);
|
|
3989
|
+
const dbViews = await this.adapter.views.findByObjectName(tenantId, objectName);
|
|
3990
|
+
const customViews = dbViews.filter((v) => !v.system).map(this.convertDBViewToDefinition);
|
|
3991
|
+
return [...nativeViewDefs, ...customViews];
|
|
3992
|
+
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Get a specific view by name
|
|
3995
|
+
*
|
|
3996
|
+
* @param objectName - Object name
|
|
3997
|
+
* @param viewName - View name
|
|
3998
|
+
* @param tenantId - Tenant ID
|
|
3999
|
+
* @returns View definition or null
|
|
4000
|
+
*/
|
|
4001
|
+
async getView(objectName, viewName, tenantId) {
|
|
4002
|
+
const nativeView = this.nativeViews.get(objectName, viewName);
|
|
4003
|
+
if (nativeView) {
|
|
4004
|
+
return nativeView;
|
|
4005
|
+
}
|
|
4006
|
+
const dbView = await this.adapter.views.findByName(tenantId, objectName, viewName);
|
|
4007
|
+
if (dbView) {
|
|
4008
|
+
return this.convertDBViewToDefinition(dbView);
|
|
4009
|
+
}
|
|
4010
|
+
return null;
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* Get the default view for an object
|
|
4014
|
+
*
|
|
4015
|
+
* Priority:
|
|
4016
|
+
* 1. Custom view marked as default
|
|
4017
|
+
* 2. Native view marked as default
|
|
4018
|
+
* 3. First available view
|
|
4019
|
+
*
|
|
4020
|
+
* @param objectName - Object name
|
|
4021
|
+
* @param tenantId - Tenant ID
|
|
4022
|
+
* @returns Default view or null
|
|
4023
|
+
*/
|
|
4024
|
+
async getDefaultView(objectName, tenantId) {
|
|
4025
|
+
const views = await this.getViewsForObject(objectName, tenantId);
|
|
4026
|
+
const customDefault = views.find((v) => v.default && !v.system);
|
|
4027
|
+
if (customDefault) return customDefault;
|
|
4028
|
+
const nativeDefault = views.find((v) => v.default && v.system);
|
|
4029
|
+
if (nativeDefault) return nativeDefault;
|
|
4030
|
+
return views[0] ?? null;
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Create a custom view
|
|
4034
|
+
*
|
|
4035
|
+
* @param input - View definition
|
|
4036
|
+
* @param tenantId - Tenant ID
|
|
4037
|
+
* @returns Created view
|
|
4038
|
+
*/
|
|
4039
|
+
async createView(input, tenantId) {
|
|
4040
|
+
this.validateViewName(input.name);
|
|
4041
|
+
const existing = await this.adapter.views.findByName(tenantId, input.objectName, input.name);
|
|
4042
|
+
if (existing) {
|
|
4043
|
+
throw new Error(`View "${input.name}" already exists for object "${input.objectName}"`);
|
|
4044
|
+
}
|
|
4045
|
+
if (this.nativeViews.has(input.objectName, input.name)) {
|
|
4046
|
+
throw new Error(
|
|
4047
|
+
`Cannot create view "${input.name}": a system view with this name already exists`
|
|
4048
|
+
);
|
|
4049
|
+
}
|
|
4050
|
+
if (!input.tabs || input.tabs.length === 0) {
|
|
4051
|
+
throw new Error("View must have at least one tab");
|
|
4052
|
+
}
|
|
4053
|
+
const dbView = await this.adapter.views.create({
|
|
4054
|
+
tenantId,
|
|
4055
|
+
objectName: input.objectName,
|
|
4056
|
+
name: input.name,
|
|
4057
|
+
label: input.label,
|
|
4058
|
+
description: input.description,
|
|
4059
|
+
icon: input.icon,
|
|
4060
|
+
tabs: input.tabs,
|
|
4061
|
+
default: input.default ?? false,
|
|
4062
|
+
system: false,
|
|
4063
|
+
// Custom views are never system
|
|
4064
|
+
metadata: input.metadata
|
|
4065
|
+
});
|
|
4066
|
+
return this.convertDBViewToDefinition(dbView);
|
|
4067
|
+
}
|
|
4068
|
+
/**
|
|
4069
|
+
* Update a custom view
|
|
4070
|
+
*
|
|
4071
|
+
* @param viewId - View ID
|
|
4072
|
+
* @param input - Update data
|
|
4073
|
+
* @returns Updated view
|
|
4074
|
+
*/
|
|
4075
|
+
async updateView(viewId, input) {
|
|
4076
|
+
const dbView = await this.adapter.views.findById(viewId);
|
|
4077
|
+
if (!dbView) {
|
|
4078
|
+
throw new Error(`View with id "${viewId}" not found`);
|
|
4079
|
+
}
|
|
4080
|
+
if (dbView.system) {
|
|
4081
|
+
throw new Error("Cannot modify system views. System views are protected.");
|
|
4082
|
+
}
|
|
4083
|
+
if (input.tabs && input.tabs.length === 0) {
|
|
4084
|
+
throw new Error("View must have at least one tab");
|
|
4085
|
+
}
|
|
4086
|
+
const updated = await this.adapter.views.update(viewId, {
|
|
4087
|
+
label: input.label,
|
|
4088
|
+
description: input.description,
|
|
4089
|
+
icon: input.icon,
|
|
4090
|
+
tabs: input.tabs,
|
|
4091
|
+
default: input.default,
|
|
4092
|
+
metadata: input.metadata
|
|
4093
|
+
});
|
|
4094
|
+
return this.convertDBViewToDefinition(updated);
|
|
4095
|
+
}
|
|
4096
|
+
/**
|
|
4097
|
+
* Delete a custom view
|
|
4098
|
+
*
|
|
4099
|
+
* @param viewId - View ID
|
|
4100
|
+
*/
|
|
4101
|
+
async deleteView(viewId) {
|
|
4102
|
+
const dbView = await this.adapter.views.findById(viewId);
|
|
4103
|
+
if (!dbView) {
|
|
4104
|
+
throw new Error(`View with id "${viewId}" not found`);
|
|
4105
|
+
}
|
|
4106
|
+
if (dbView.system) {
|
|
4107
|
+
throw new Error("Cannot delete system views. System views are protected.");
|
|
4108
|
+
}
|
|
4109
|
+
await this.adapter.views.delete(viewId);
|
|
4110
|
+
}
|
|
4111
|
+
/**
|
|
4112
|
+
* Set a view as default for its object
|
|
4113
|
+
*
|
|
4114
|
+
* @param viewId - View ID
|
|
4115
|
+
* @param tenantId - Tenant ID
|
|
4116
|
+
* @returns Updated view
|
|
4117
|
+
*/
|
|
4118
|
+
async setDefaultView(viewId, tenantId) {
|
|
4119
|
+
const dbView = await this.adapter.views.findById(viewId);
|
|
4120
|
+
if (!dbView) {
|
|
4121
|
+
throw new Error(`View with id "${viewId}" not found`);
|
|
4122
|
+
}
|
|
4123
|
+
const currentViews = await this.adapter.views.findByObjectName(tenantId, dbView.objectName);
|
|
4124
|
+
for (const v of currentViews) {
|
|
4125
|
+
if (v.default && v.id !== viewId && !v.system) {
|
|
4126
|
+
await this.adapter.views.update(v.id, { default: false });
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
const updated = await this.adapter.views.update(viewId, { default: true });
|
|
4130
|
+
return this.convertDBViewToDefinition(updated);
|
|
4131
|
+
}
|
|
4132
|
+
// ============================================================================
|
|
4133
|
+
// PRIVATE HELPERS
|
|
4134
|
+
// ============================================================================
|
|
4135
|
+
/**
|
|
4136
|
+
* Validate view name format (kebab-case)
|
|
4137
|
+
*/
|
|
4138
|
+
validateViewName(name) {
|
|
4139
|
+
if (!name || name.length === 0) {
|
|
4140
|
+
throw new Error("View name cannot be empty");
|
|
4141
|
+
}
|
|
4142
|
+
if (name.length > 63) {
|
|
4143
|
+
throw new Error("View name is too long (max 63 characters)");
|
|
4144
|
+
}
|
|
4145
|
+
const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
|
|
4146
|
+
if (!kebabCaseRegex.test(name)) {
|
|
4147
|
+
throw new Error(
|
|
4148
|
+
"Invalid view name format. Name must be in kebab-case (e.g., 'detail', 'list-view')"
|
|
4149
|
+
);
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* Convert database view to ViewDefinition
|
|
4154
|
+
*/
|
|
4155
|
+
convertDBViewToDefinition(dbView) {
|
|
4156
|
+
return {
|
|
4157
|
+
id: dbView.id,
|
|
4158
|
+
name: dbView.name,
|
|
4159
|
+
label: dbView.label,
|
|
4160
|
+
description: dbView.description,
|
|
4161
|
+
icon: dbView.icon,
|
|
4162
|
+
object: dbView.objectName,
|
|
4163
|
+
tabs: dbView.tabs,
|
|
4164
|
+
default: dbView.default,
|
|
4165
|
+
system: dbView.system,
|
|
4166
|
+
metadata: dbView.metadata
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
};
|
|
4170
|
+
|
|
4171
|
+
// src/runtime/view-sync.ts
|
|
4172
|
+
async function syncNativeViews(adapter, nativeViewRegistry, options = {}) {
|
|
4173
|
+
const result = {
|
|
4174
|
+
success: true,
|
|
4175
|
+
viewsSynced: 0,
|
|
4176
|
+
viewsCreated: 0,
|
|
4177
|
+
viewsUpdated: 0,
|
|
4178
|
+
viewsDeleted: 0,
|
|
4179
|
+
errors: []
|
|
4180
|
+
};
|
|
4181
|
+
const nativeViews = nativeViewRegistry.getAll();
|
|
4182
|
+
if (options.verbose) {
|
|
4183
|
+
console.info(`[ViewSync] Starting sync for ${nativeViews.length} native views...`);
|
|
4184
|
+
}
|
|
4185
|
+
try {
|
|
4186
|
+
await adapter.transaction(async (tx) => {
|
|
4187
|
+
const viewsByObject = await syncAllViews(tx, nativeViews, result, options);
|
|
4188
|
+
await cleanupRemovedViews(tx, viewsByObject, result, options);
|
|
4189
|
+
});
|
|
4190
|
+
} catch (error) {
|
|
4191
|
+
handleTransactionError(result, error);
|
|
4192
|
+
}
|
|
4193
|
+
logSyncComplete(result, options);
|
|
4194
|
+
return result;
|
|
4195
|
+
}
|
|
4196
|
+
async function syncAllViews(tx, nativeViews, result, options) {
|
|
4197
|
+
const viewsByObject = /* @__PURE__ */ new Map();
|
|
4198
|
+
for (const nativeView of nativeViews) {
|
|
4199
|
+
try {
|
|
4200
|
+
await syncSingleView(tx, nativeView, result, options);
|
|
4201
|
+
trackViewByObject(viewsByObject, nativeView);
|
|
4202
|
+
} catch (error) {
|
|
4203
|
+
handleViewSyncError(result, nativeView, error, options);
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
return viewsByObject;
|
|
4207
|
+
}
|
|
4208
|
+
function trackViewByObject(viewsByObject, nativeView) {
|
|
4209
|
+
const objectViews = viewsByObject.get(nativeView.object) ?? [];
|
|
4210
|
+
objectViews.push(nativeView.name);
|
|
4211
|
+
viewsByObject.set(nativeView.object, objectViews);
|
|
4212
|
+
}
|
|
4213
|
+
function handleViewSyncError(result, nativeView, error, options) {
|
|
4214
|
+
result.success = false;
|
|
4215
|
+
result.errors.push({
|
|
4216
|
+
viewName: nativeView.name,
|
|
4217
|
+
objectName: nativeView.object,
|
|
4218
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4219
|
+
});
|
|
4220
|
+
if (options.verbose) {
|
|
4221
|
+
console.error(`[ViewSync] \u2717 Failed to sync ${nativeView.object}:${nativeView.name}:`, error);
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
async function cleanupRemovedViews(tx, viewsByObject, result, options) {
|
|
4225
|
+
if (options.dryRun) return;
|
|
4226
|
+
for (const [objectName, viewNames] of viewsByObject) {
|
|
4227
|
+
const deletedCount = await tx.views.deleteNotIn(objectName, viewNames);
|
|
4228
|
+
result.viewsDeleted += deletedCount;
|
|
4229
|
+
if (options.verbose && deletedCount > 0) {
|
|
4230
|
+
console.info(`[ViewSync] Deleted ${deletedCount} obsolete views for ${objectName}`);
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
function handleTransactionError(result, error) {
|
|
4235
|
+
result.success = false;
|
|
4236
|
+
result.errors.push({
|
|
4237
|
+
viewName: "transaction",
|
|
4238
|
+
objectName: "",
|
|
4239
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4240
|
+
});
|
|
4241
|
+
}
|
|
4242
|
+
function logSyncComplete(result, options) {
|
|
4243
|
+
if (options.verbose) {
|
|
4244
|
+
console.info(
|
|
4245
|
+
`[ViewSync] ${result.success ? "\u2713" : "\u2717"} Sync complete:
|
|
4246
|
+
Views: ${result.viewsCreated} created, ${result.viewsUpdated} updated, ${result.viewsDeleted} deleted
|
|
4247
|
+
Errors: ${result.errors.length}`
|
|
4248
|
+
);
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
async function syncSingleView(adapter, nativeView, result, options) {
|
|
4252
|
+
if (!nativeView.system) {
|
|
4253
|
+
throw new Error(`View ${nativeView.name} is not marked as system`);
|
|
4254
|
+
}
|
|
4255
|
+
const existingView = await adapter.views.findSystemByName(nativeView.object, nativeView.name);
|
|
4256
|
+
const isNew = !existingView;
|
|
4257
|
+
if (isNew) {
|
|
4258
|
+
result.viewsCreated++;
|
|
4259
|
+
} else {
|
|
4260
|
+
result.viewsUpdated++;
|
|
4261
|
+
}
|
|
4262
|
+
result.viewsSynced++;
|
|
4263
|
+
if (options.dryRun) {
|
|
4264
|
+
if (options.verbose) {
|
|
4265
|
+
console.info(
|
|
4266
|
+
`[ViewSync] Would ${isNew ? "create" : "update"} view: ${nativeView.object}:${nativeView.name}`
|
|
4267
|
+
);
|
|
4268
|
+
}
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
await adapter.views.upsert({
|
|
4272
|
+
tenantId: options.tenantId || "default",
|
|
4273
|
+
objectName: nativeView.object,
|
|
4274
|
+
name: nativeView.name,
|
|
4275
|
+
label: nativeView.label,
|
|
4276
|
+
description: nativeView.description,
|
|
4277
|
+
icon: nativeView.icon,
|
|
4278
|
+
tabs: nativeView.tabs,
|
|
4279
|
+
default: nativeView.default ?? false,
|
|
4280
|
+
system: true,
|
|
4281
|
+
metadata: nativeView.metadata
|
|
4282
|
+
});
|
|
4283
|
+
if (options.verbose) {
|
|
4284
|
+
console.info(
|
|
4285
|
+
`[ViewSync] \u2713 Synced ${nativeView.object}:${nativeView.name}: ${nativeView.tabs.length} tabs`
|
|
4286
|
+
);
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
async function verifyNativeViewsSync(adapter, nativeViewRegistry) {
|
|
4290
|
+
const nativeViews = nativeViewRegistry.getAll();
|
|
4291
|
+
for (const view2 of nativeViews) {
|
|
4292
|
+
if (!view2.system) continue;
|
|
4293
|
+
const existing = await adapter.views.findSystemByName(view2.object, view2.name);
|
|
4294
|
+
if (!existing) {
|
|
4295
|
+
return false;
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
return true;
|
|
4299
|
+
}
|
|
4300
|
+
async function getViewSyncPreview(adapter, nativeViewRegistry) {
|
|
4301
|
+
return await syncNativeViews(adapter, nativeViewRegistry, { dryRun: true });
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
// src/runtime/sync.ts
|
|
4305
|
+
var BASE_ATTRIBUTE_KEYS = /* @__PURE__ */ new Set([
|
|
4306
|
+
"id",
|
|
4307
|
+
"name",
|
|
4308
|
+
"label",
|
|
4309
|
+
"type",
|
|
4310
|
+
"required",
|
|
4311
|
+
"system",
|
|
4312
|
+
"unique",
|
|
4313
|
+
"order"
|
|
4314
|
+
]);
|
|
4315
|
+
function extractAttributeConfig(attr) {
|
|
4316
|
+
return Object.fromEntries(
|
|
4317
|
+
Object.entries(attr).filter(
|
|
4318
|
+
([key, value]) => !BASE_ATTRIBUTE_KEYS.has(key) && value !== void 0
|
|
4319
|
+
)
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
async function syncNativeObjects(adapter, nativeRegistry, options = {}) {
|
|
4323
|
+
const result = {
|
|
4324
|
+
success: true,
|
|
4325
|
+
objectsSynced: 0,
|
|
4326
|
+
attributesSynced: 0,
|
|
4327
|
+
objectsCreated: 0,
|
|
4328
|
+
objectsUpdated: 0,
|
|
4329
|
+
attributesCreated: 0,
|
|
4330
|
+
attributesUpdated: 0,
|
|
4331
|
+
attributesDeleted: 0,
|
|
4332
|
+
errors: []
|
|
4333
|
+
};
|
|
4334
|
+
const nativeObjects = nativeRegistry.getAll();
|
|
4335
|
+
if (options.verbose) {
|
|
4336
|
+
console.info(`[SyncService] Starting sync for ${nativeObjects.length} native objects...`);
|
|
4337
|
+
}
|
|
4338
|
+
try {
|
|
4339
|
+
await adapter.transaction(async (tx) => {
|
|
4340
|
+
for (const nativeObject of nativeObjects) {
|
|
4341
|
+
try {
|
|
4342
|
+
await syncSingleObject(tx, nativeObject, result, options);
|
|
4343
|
+
} catch (error) {
|
|
4344
|
+
result.success = false;
|
|
4345
|
+
result.errors.push({
|
|
4346
|
+
objectName: nativeObject.name,
|
|
4347
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4348
|
+
});
|
|
4349
|
+
if (options.verbose) {
|
|
4350
|
+
console.error(`[SyncService] \u2717 Failed to sync ${nativeObject.name}:`, error);
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
});
|
|
4355
|
+
} catch (error) {
|
|
4356
|
+
result.success = false;
|
|
4357
|
+
result.errors.push({
|
|
4358
|
+
objectName: "transaction",
|
|
4359
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4360
|
+
});
|
|
4361
|
+
}
|
|
4362
|
+
if (options.verbose) {
|
|
4363
|
+
console.info(
|
|
4364
|
+
`[SyncService] ${result.success ? "\u2713" : "\u2717"} Sync complete:
|
|
4365
|
+
Objects: ${result.objectsCreated} created, ${result.objectsUpdated} updated
|
|
4366
|
+
Attributes: ${result.attributesCreated} created, ${result.attributesUpdated} updated, ${result.attributesDeleted} deleted
|
|
4367
|
+
Errors: ${result.errors.length}`
|
|
4368
|
+
);
|
|
4369
|
+
}
|
|
4370
|
+
return result;
|
|
4371
|
+
}
|
|
4372
|
+
async function syncSingleObject(adapter, nativeObject, result, options) {
|
|
4373
|
+
if (!nativeObject.system) {
|
|
4374
|
+
throw new Error(`Object ${nativeObject.name} is not marked as system`);
|
|
4375
|
+
}
|
|
4376
|
+
const existingObject = await adapter.objects.findSystemByName(nativeObject.name);
|
|
4377
|
+
const isNew = !existingObject;
|
|
4378
|
+
updateObjectStats(result, isNew);
|
|
4379
|
+
if (options.dryRun) {
|
|
4380
|
+
await handleDryRun(adapter, nativeObject, existingObject, result, options, isNew);
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
const dbObject = await upsertObject(adapter, nativeObject, options);
|
|
4384
|
+
await syncAttributes(adapter, nativeObject, dbObject, existingObject, result);
|
|
4385
|
+
if (!isNew) {
|
|
4386
|
+
await cleanupAttributes(adapter, dbObject.id, nativeObject, result);
|
|
4387
|
+
}
|
|
4388
|
+
logSyncSuccess(nativeObject, options);
|
|
4389
|
+
}
|
|
4390
|
+
function updateObjectStats(result, isNew) {
|
|
4391
|
+
if (isNew) {
|
|
4392
|
+
result.objectsCreated++;
|
|
4393
|
+
} else {
|
|
4394
|
+
result.objectsUpdated++;
|
|
4395
|
+
}
|
|
4396
|
+
result.objectsSynced++;
|
|
4397
|
+
}
|
|
4398
|
+
async function handleDryRun(adapter, nativeObject, existingObject, result, options, isNew) {
|
|
4399
|
+
if (options.verbose) {
|
|
4400
|
+
console.info(`[SyncService] Would ${isNew ? "create" : "update"} object: ${nativeObject.name}`);
|
|
4401
|
+
}
|
|
4402
|
+
for (const attr of nativeObject.attributes) {
|
|
4403
|
+
const existingAttr = existingObject ? await adapter.attributes.findByObjectId(existingObject.id).then((attrs) => attrs.find((a) => a.name === attr.name)) : null;
|
|
4404
|
+
if (existingAttr) {
|
|
4405
|
+
result.attributesUpdated++;
|
|
4406
|
+
} else {
|
|
4407
|
+
result.attributesCreated++;
|
|
4408
|
+
}
|
|
4409
|
+
result.attributesSynced++;
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
async function upsertObject(adapter, nativeObject, options) {
|
|
4413
|
+
return await adapter.objects.upsert({
|
|
4414
|
+
tenantId: options.tenantId || "default",
|
|
4415
|
+
system: true,
|
|
4416
|
+
name: nativeObject.name,
|
|
4417
|
+
label: nativeObject.label,
|
|
4418
|
+
description: nativeObject.description,
|
|
4419
|
+
labelExpression: nativeObject.labelExpression,
|
|
4420
|
+
icon: nativeObject.icon,
|
|
4421
|
+
metadata: nativeObject.metadata
|
|
4422
|
+
});
|
|
4423
|
+
}
|
|
4424
|
+
async function syncAttributes(adapter, nativeObject, dbObject, existingObject, result) {
|
|
4425
|
+
for (const [index, attr] of nativeObject.attributes.entries()) {
|
|
4426
|
+
const existingAttr = existingObject ? await adapter.attributes.findByObjectId(dbObject.id).then((attrs) => attrs.find((a) => a.name === attr.name)) : null;
|
|
4427
|
+
await adapter.attributes.upsert({
|
|
4428
|
+
objectId: dbObject.id,
|
|
4429
|
+
name: attr.name,
|
|
4430
|
+
label: attr.label,
|
|
4431
|
+
type: attr.type,
|
|
4432
|
+
description: attr.description,
|
|
4433
|
+
icon: attr.icon,
|
|
4434
|
+
system: attr.system ?? true,
|
|
4435
|
+
required: attr.required ?? false,
|
|
4436
|
+
order: attr.order ?? index,
|
|
4437
|
+
hidden: attr.hidden,
|
|
4438
|
+
archived: attr.archived,
|
|
4439
|
+
deprecated: attr.deprecated,
|
|
4440
|
+
defaultValue: attr.defaultValue,
|
|
4441
|
+
config: extractAttributeConfig(attr)
|
|
4442
|
+
});
|
|
4443
|
+
if (existingAttr) {
|
|
4444
|
+
result.attributesUpdated++;
|
|
4445
|
+
} else {
|
|
4446
|
+
result.attributesCreated++;
|
|
4447
|
+
}
|
|
4448
|
+
result.attributesSynced++;
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
async function cleanupAttributes(adapter, objectId, nativeObject, result) {
|
|
4452
|
+
const attributeNames = nativeObject.attributes.map((attr) => attr.name);
|
|
4453
|
+
const deletedCount = await adapter.attributes.deleteByNames(objectId, attributeNames);
|
|
4454
|
+
result.attributesDeleted += deletedCount;
|
|
4455
|
+
}
|
|
4456
|
+
function logSyncSuccess(nativeObject, options) {
|
|
4457
|
+
if (options.verbose) {
|
|
4458
|
+
console.info(
|
|
4459
|
+
`[SyncService] \u2713 Synced ${nativeObject.name}: ${nativeObject.attributes.length} attributes`
|
|
4460
|
+
);
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
async function verifyNativeObjectsSync(adapter, nativeRegistry) {
|
|
4464
|
+
const nativeObjects = nativeRegistry.getAll();
|
|
4465
|
+
for (const obj of nativeObjects) {
|
|
4466
|
+
if (!obj.system) continue;
|
|
4467
|
+
const existing = await adapter.objects.findSystemByName(obj.name);
|
|
4468
|
+
if (!existing) {
|
|
4469
|
+
return false;
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
return true;
|
|
4473
|
+
}
|
|
4474
|
+
async function getSyncPreview(adapter, nativeRegistry) {
|
|
4475
|
+
return await syncNativeObjects(adapter, nativeRegistry, { dryRun: true });
|
|
4476
|
+
}
|
|
4477
|
+
async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}) {
|
|
4478
|
+
if (options.verbose) {
|
|
4479
|
+
console.info("[SyncAll] Starting full sync...");
|
|
4480
|
+
}
|
|
4481
|
+
const objectsResult = await syncNativeObjects(adapter, objectRegistry, options);
|
|
4482
|
+
const viewsResult = await syncNativeViews(adapter, nativeViewRegistry, options);
|
|
4483
|
+
const result = {
|
|
4484
|
+
success: objectsResult.success && viewsResult.success,
|
|
4485
|
+
objects: objectsResult,
|
|
4486
|
+
views: viewsResult
|
|
4487
|
+
};
|
|
4488
|
+
if (options.verbose) {
|
|
4489
|
+
console.info(
|
|
4490
|
+
`[SyncAll] ${result.success ? "\u2713" : "\u2717"} Full sync complete:
|
|
4491
|
+
Objects: ${objectsResult.objectsSynced} synced (${objectsResult.objectsCreated} created, ${objectsResult.objectsUpdated} updated)
|
|
4492
|
+
Views: ${viewsResult.viewsSynced} synced (${viewsResult.viewsCreated} created, ${viewsResult.viewsUpdated} updated)
|
|
4493
|
+
Errors: ${objectsResult.errors.length + viewsResult.errors.length}`
|
|
4494
|
+
);
|
|
4495
|
+
}
|
|
4496
|
+
return result;
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
// src/types/geocoding.ts
|
|
4500
|
+
var NoopGeocodingAdapter = class {
|
|
4501
|
+
async autocomplete() {
|
|
4502
|
+
return await Promise.resolve([]);
|
|
4503
|
+
}
|
|
4504
|
+
async reverse() {
|
|
4505
|
+
return await Promise.resolve(null);
|
|
4506
|
+
}
|
|
4507
|
+
async geocode() {
|
|
4508
|
+
return await Promise.resolve(null);
|
|
4509
|
+
}
|
|
4510
|
+
};
|
|
4511
|
+
|
|
4512
|
+
export {
|
|
4513
|
+
NoopGeocodingAdapter,
|
|
4514
|
+
generateId,
|
|
4515
|
+
generatePrefixedId,
|
|
4516
|
+
text,
|
|
4517
|
+
textarea,
|
|
4518
|
+
number,
|
|
4519
|
+
checkbox,
|
|
4520
|
+
date,
|
|
4521
|
+
phone,
|
|
4522
|
+
currency,
|
|
4523
|
+
status,
|
|
4524
|
+
select,
|
|
4525
|
+
multiselect,
|
|
4526
|
+
location,
|
|
4527
|
+
timestamp,
|
|
4528
|
+
file,
|
|
4529
|
+
user,
|
|
4530
|
+
relation,
|
|
4531
|
+
rating,
|
|
4532
|
+
ObjectBuilder,
|
|
4533
|
+
object,
|
|
4534
|
+
GroupBuilder,
|
|
4535
|
+
TableTabConfig,
|
|
4536
|
+
CustomTabConfig,
|
|
4537
|
+
TabBuilder,
|
|
4538
|
+
ViewBuilder,
|
|
4539
|
+
view,
|
|
4540
|
+
group,
|
|
4541
|
+
registry,
|
|
4542
|
+
textConfigSchema,
|
|
4543
|
+
textareaConfigSchema,
|
|
4544
|
+
numberConfigSchema,
|
|
4545
|
+
checkboxConfigSchema,
|
|
4546
|
+
dateConfigSchema,
|
|
4547
|
+
phoneConfigSchema,
|
|
4548
|
+
currencyConfigSchema,
|
|
4549
|
+
statusConfigSchema,
|
|
4550
|
+
locationConfigSchema,
|
|
4551
|
+
timestampConfigSchema,
|
|
4552
|
+
selectConfigSchema,
|
|
4553
|
+
multiselectConfigSchema,
|
|
4554
|
+
fileConfigSchema,
|
|
4555
|
+
userConfigSchema,
|
|
4556
|
+
relationConfigSchema,
|
|
4557
|
+
ratingConfigSchema,
|
|
4558
|
+
attributeConfigSchemas,
|
|
4559
|
+
getAttributeConfigSchema,
|
|
4560
|
+
validateAttributeConfig,
|
|
4561
|
+
parseAttributeConfig,
|
|
4562
|
+
safeParseAttributeConfig,
|
|
4563
|
+
createTextValidator,
|
|
4564
|
+
createNumberValidator,
|
|
4565
|
+
createCheckboxValidator,
|
|
4566
|
+
createDateValidator,
|
|
4567
|
+
createPhoneValidator,
|
|
4568
|
+
createCurrencyValidator,
|
|
4569
|
+
createStatusValidator,
|
|
4570
|
+
createSelectValidator,
|
|
4571
|
+
createMultiselectValidator,
|
|
4572
|
+
createLocationValidator,
|
|
4573
|
+
createTimestampValidator,
|
|
4574
|
+
createFileValidator,
|
|
4575
|
+
createUserValidator,
|
|
4576
|
+
createSingleRelationValidator,
|
|
4577
|
+
createMultiRelationValidator,
|
|
4578
|
+
createRelationValidator,
|
|
4579
|
+
createRatingValidator,
|
|
4580
|
+
createAttributeValidator,
|
|
4581
|
+
createObjectValidator,
|
|
4582
|
+
validateAttribute,
|
|
4583
|
+
validateObject,
|
|
4584
|
+
validateObjectOrThrow,
|
|
4585
|
+
createDraftValidator,
|
|
4586
|
+
validateDraft,
|
|
4587
|
+
validateDraftOrThrow,
|
|
4588
|
+
getMissingRequiredAttributes,
|
|
4589
|
+
isRecordComplete,
|
|
4590
|
+
computeRecordStatus,
|
|
4591
|
+
NoopHookRegistry,
|
|
4592
|
+
DEFAULT_LABEL_FALLBACK,
|
|
4593
|
+
renderLabelExpression,
|
|
4594
|
+
isLabelExpression,
|
|
4595
|
+
extractAttributeNames,
|
|
4596
|
+
createMockAdapter,
|
|
4597
|
+
FileService,
|
|
4598
|
+
GeocodingService,
|
|
4599
|
+
GlobalSearchService,
|
|
4600
|
+
ObjectSchemaService,
|
|
4601
|
+
RelationService,
|
|
4602
|
+
RecordService,
|
|
4603
|
+
UserProfileService,
|
|
4604
|
+
ViewService,
|
|
4605
|
+
syncNativeViews,
|
|
4606
|
+
verifyNativeViewsSync,
|
|
4607
|
+
getViewSyncPreview,
|
|
4608
|
+
syncNativeObjects,
|
|
4609
|
+
verifyNativeObjectsSync,
|
|
4610
|
+
getSyncPreview,
|
|
4611
|
+
syncAll
|
|
4612
|
+
};
|