@fayz-ai/core 0.11.0 → 0.14.0
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-BEGXYHS4.js → chunk-7GJTLBG3.js} +3 -3
- package/dist/{chunk-BEGXYHS4.js.map → chunk-7GJTLBG3.js.map} +1 -1
- package/dist/{chunk-V53GR4UT.js → chunk-IKZR3WSK.js} +3 -3
- package/dist/{chunk-V53GR4UT.js.map → chunk-IKZR3WSK.js.map} +1 -1
- package/dist/{chunk-LARZWDHA.js → chunk-KFBEKFII.js} +5 -4
- package/dist/chunk-KFBEKFII.js.map +1 -0
- package/dist/chunk-S5FOFKR4.js +1618 -0
- package/dist/chunk-S5FOFKR4.js.map +1 -0
- package/dist/{chunk-TDI6F6PT.js → chunk-X4FRQR2R.js} +10 -3
- package/dist/chunk-X4FRQR2R.js.map +1 -0
- package/dist/data/cached.d.ts +5 -1
- package/dist/data/cached.d.ts.map +1 -1
- package/dist/data/errors.d.ts +44 -0
- package/dist/data/errors.d.ts.map +1 -0
- package/dist/data/index.d.ts +8 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +2 -2
- package/dist/data/offline.d.ts +45 -0
- package/dist/data/offline.d.ts.map +1 -0
- package/dist/data/query.d.ts +91 -0
- package/dist/data/query.d.ts.map +1 -0
- package/dist/data/resolve.d.ts.map +1 -1
- package/dist/data/types.d.ts +13 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/events/bus.d.ts +12 -0
- package/dist/events/bus.d.ts.map +1 -0
- package/dist/events/index.d.ts +3 -11
- package/dist/events/index.d.ts.map +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/shell-translations.en.d.ts.map +1 -1
- package/dist/i18n/shell-translations.pt-br.d.ts.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +191 -268
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.js +2 -2
- package/dist/lib/cache-idb.d.ts +54 -0
- package/dist/lib/cache-idb.d.ts.map +1 -0
- package/dist/lib/cache.d.ts +2 -0
- package/dist/lib/cache.d.ts.map +1 -1
- package/dist/search/engine.d.ts.map +1 -1
- package/dist/search/types.d.ts +7 -0
- package/dist/search/types.d.ts.map +1 -1
- package/dist/{shell-translations.pt-br-BM5ZFSKJ.js → shell-translations.pt-br-PTOWYXNA.js} +7 -2
- package/dist/shell-translations.pt-br-PTOWYXNA.js.map +1 -0
- package/dist/testing/index.js +3 -3
- package/dist/types/analytics.d.ts +22 -0
- package/dist/types/analytics.d.ts.map +1 -1
- package/dist/types/crud.d.ts +20 -0
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +11 -0
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-LARZWDHA.js.map +0 -1
- package/dist/chunk-TDI6F6PT.js.map +0 -1
- package/dist/chunk-UVKH6RDX.js +0 -739
- package/dist/chunk-UVKH6RDX.js.map +0 -1
- package/dist/shell-translations.pt-br-BM5ZFSKJ.js.map +0 -1
|
@@ -0,0 +1,1618 @@
|
|
|
1
|
+
import { globalCache, getActiveTenantId, getSupabaseClientOptional, parseRangeFilter, matchesRange, applyRangeToQuery, isFilterObject, stableKey, DEFAULT_CACHE_TTL, createSupabaseProvider } from './chunk-KFBEKFII.js';
|
|
2
|
+
import { declaredCustomFields, validateEntityRecordData } from './chunk-EWKX4VUM.js';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
// src/data/count.ts
|
|
6
|
+
var TTL_MS = 15e3;
|
|
7
|
+
var cache = /* @__PURE__ */ new Map();
|
|
8
|
+
function cacheKey(table, tenantId, kind, period) {
|
|
9
|
+
return `${tenantId}::${table}::${kind ?? ""}::${period}`;
|
|
10
|
+
}
|
|
11
|
+
function startOfMonthISO() {
|
|
12
|
+
const now = /* @__PURE__ */ new Date();
|
|
13
|
+
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
|
|
14
|
+
}
|
|
15
|
+
var COUNT_TIMEOUT_MS = 5e3;
|
|
16
|
+
async function countByTenant(table, options = {}) {
|
|
17
|
+
const tenantId = options.tenantId ?? getActiveTenantId();
|
|
18
|
+
if (!tenantId) return 0;
|
|
19
|
+
const period = options.period ?? "total";
|
|
20
|
+
const key = cacheKey(table, tenantId, options.kind, period);
|
|
21
|
+
if (!options.fresh) {
|
|
22
|
+
const hit = cache.get(key);
|
|
23
|
+
if (hit && hit.expires > Date.now()) return hit.value;
|
|
24
|
+
}
|
|
25
|
+
const client = getSupabaseClientOptional();
|
|
26
|
+
if (!client) return 0;
|
|
27
|
+
let query = client.from(table).select("*", { count: "exact", head: true }).eq("tenant_id", tenantId);
|
|
28
|
+
if (options.kind) {
|
|
29
|
+
query = query.eq("kind", options.kind);
|
|
30
|
+
}
|
|
31
|
+
if (period === "month") {
|
|
32
|
+
query = query.gte("created_at", startOfMonthISO());
|
|
33
|
+
}
|
|
34
|
+
let timer;
|
|
35
|
+
const timeout = new Promise((resolve) => {
|
|
36
|
+
timer = setTimeout(() => resolve({ count: null, error: "timeout", timedOut: true }), COUNT_TIMEOUT_MS);
|
|
37
|
+
});
|
|
38
|
+
const settled = await Promise.race([
|
|
39
|
+
query,
|
|
40
|
+
timeout
|
|
41
|
+
]);
|
|
42
|
+
if (timer) clearTimeout(timer);
|
|
43
|
+
const { count, error } = settled;
|
|
44
|
+
const value = error ? 0 : count ?? 0;
|
|
45
|
+
if (!settled.timedOut) {
|
|
46
|
+
cache.set(key, { value, expires: Date.now() + TTL_MS });
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function invalidateCount(tenantOrKey) {
|
|
51
|
+
if (!tenantOrKey) {
|
|
52
|
+
cache.clear();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const key of cache.keys()) {
|
|
56
|
+
if (key.includes(tenantOrKey)) cache.delete(key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/data/platform-api.ts
|
|
61
|
+
function resolveTenantId(config) {
|
|
62
|
+
const tenantId = config?.tenantId;
|
|
63
|
+
return typeof tenantId === "function" ? tenantId() : tenantId;
|
|
64
|
+
}
|
|
65
|
+
async function resolveRuntimeToken(config) {
|
|
66
|
+
const runtimeToken = config?.runtimeToken;
|
|
67
|
+
return typeof runtimeToken === "function" ? runtimeToken() : runtimeToken;
|
|
68
|
+
}
|
|
69
|
+
function trimTrailingSlash(value) {
|
|
70
|
+
return value.replace(/\/+$/, "");
|
|
71
|
+
}
|
|
72
|
+
function appendQuery(url, query, tenantId, tenantIdColumn = "tenant_id", searchColumns = []) {
|
|
73
|
+
const filters = normalizeFilters(query.filters);
|
|
74
|
+
if (query.search && searchColumns[0]) {
|
|
75
|
+
filters.push({ column: searchColumns[0], operator: "ilike", value: `%${query.search}%` });
|
|
76
|
+
}
|
|
77
|
+
if (query.sortBy) url.searchParams.set("sortColumn", query.sortBy);
|
|
78
|
+
if (query.sortDir) url.searchParams.set("sortDirection", query.sortDir);
|
|
79
|
+
if (query.page != null) url.searchParams.set("page", String(query.page));
|
|
80
|
+
if (query.pageSize != null) url.searchParams.set("limit", String(query.pageSize));
|
|
81
|
+
if (tenantId) filters.push({ column: tenantIdColumn, operator: "eq", value: tenantId });
|
|
82
|
+
if (filters.length > 0) url.searchParams.set("filters", JSON.stringify(filters));
|
|
83
|
+
}
|
|
84
|
+
async function parseResponse(response) {
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const text = await response.text().catch(() => "");
|
|
87
|
+
throw new Error(`[@fayz-ai/core] Fayz API request failed (${response.status}): ${text || response.statusText}`);
|
|
88
|
+
}
|
|
89
|
+
return response.json();
|
|
90
|
+
}
|
|
91
|
+
function isFilterOperator(value) {
|
|
92
|
+
return typeof value === "string" && [
|
|
93
|
+
"eq",
|
|
94
|
+
"neq",
|
|
95
|
+
"gt",
|
|
96
|
+
"lt",
|
|
97
|
+
"gte",
|
|
98
|
+
"lte",
|
|
99
|
+
"like",
|
|
100
|
+
"ilike",
|
|
101
|
+
"is_null",
|
|
102
|
+
"is_not_null"
|
|
103
|
+
].includes(value);
|
|
104
|
+
}
|
|
105
|
+
function isRecord(value) {
|
|
106
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107
|
+
}
|
|
108
|
+
function normalizeFilters(filters) {
|
|
109
|
+
if (!filters) return [];
|
|
110
|
+
return Object.entries(filters).flatMap(([column, raw]) => {
|
|
111
|
+
if (isRecord(raw) && isFilterOperator(raw.operator)) {
|
|
112
|
+
return [{ column, operator: raw.operator, value: raw.value }];
|
|
113
|
+
}
|
|
114
|
+
const range = parseRangeFilter(raw);
|
|
115
|
+
if (range) {
|
|
116
|
+
return ["gte", "gt", "lte", "lt"].filter((op) => range[op] !== void 0).map((op) => ({ column, operator: op, value: range[op] }));
|
|
117
|
+
}
|
|
118
|
+
return [{ column, operator: "eq", value: raw }];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function createFayzApiProvider(entityOrTable, config = {}) {
|
|
122
|
+
const fetcher = config.fetcher ?? fetch;
|
|
123
|
+
const baseUrl = trimTrailingSlash(config.baseUrl ?? "");
|
|
124
|
+
const projectId = config.projectId;
|
|
125
|
+
const table = config.table ?? entityOrTable;
|
|
126
|
+
const idColumn = config.idColumn ?? "id";
|
|
127
|
+
async function headers(runtimeToken) {
|
|
128
|
+
return {
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
...config.headers ? await config.headers() : {},
|
|
131
|
+
...runtimeToken ? { Authorization: `Bearer ${runtimeToken}` } : {}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function tableRowsUrl(runtime = false) {
|
|
135
|
+
const prefix = baseUrl || (typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
136
|
+
const encodedProjectId = encodeURIComponent(projectId ?? "current");
|
|
137
|
+
const encodedTable = encodeURIComponent(table);
|
|
138
|
+
const route = runtime ? `/api/v1/runtime/projects/${encodedProjectId}/database/tables/${encodedTable}/rows` : `/api/projects/${encodedProjectId}/database/tables/${encodedTable}/rows`;
|
|
139
|
+
const url = new URL(route, prefix);
|
|
140
|
+
if (config.schema) url.searchParams.set("schema", config.schema);
|
|
141
|
+
return url;
|
|
142
|
+
}
|
|
143
|
+
function withTenant(data, runtime = false) {
|
|
144
|
+
if (runtime) return data;
|
|
145
|
+
const tenantId = resolveTenantId(config);
|
|
146
|
+
if (!tenantId || config.tenantIdColumn === false) return data;
|
|
147
|
+
return { ...data, [config.tenantIdColumn ?? "tenant_id"]: tenantId };
|
|
148
|
+
}
|
|
149
|
+
function primaryKeys(id) {
|
|
150
|
+
return { [idColumn]: id };
|
|
151
|
+
}
|
|
152
|
+
async function listRows(query) {
|
|
153
|
+
const runtimeToken = await resolveRuntimeToken(config);
|
|
154
|
+
const runtime = Boolean(runtimeToken);
|
|
155
|
+
const url = tableRowsUrl(runtime);
|
|
156
|
+
appendQuery(
|
|
157
|
+
url,
|
|
158
|
+
query,
|
|
159
|
+
runtime || config.tenantIdColumn === false ? void 0 : resolveTenantId(config),
|
|
160
|
+
config.tenantIdColumn || void 0,
|
|
161
|
+
config.searchColumns
|
|
162
|
+
);
|
|
163
|
+
const result = await parseResponse(await fetcher(url, { headers: await headers(runtimeToken) }));
|
|
164
|
+
return { data: result.rows, total: result.total };
|
|
165
|
+
}
|
|
166
|
+
async function assertTenantScopedRowExists(id, runtime = false) {
|
|
167
|
+
if (runtime) return;
|
|
168
|
+
const tenantId = config.tenantIdColumn === false ? void 0 : resolveTenantId(config);
|
|
169
|
+
if (!tenantId) return;
|
|
170
|
+
const result = await listRows({ filters: { [idColumn]: id }, page: 1, pageSize: 1 });
|
|
171
|
+
if (result.data.length === 0) {
|
|
172
|
+
throw new Error(`[@fayz-ai/core] Row "${id}" was not found for the active tenant.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
async list(query) {
|
|
177
|
+
return listRows(query);
|
|
178
|
+
},
|
|
179
|
+
async create(data) {
|
|
180
|
+
const runtimeToken = await resolveRuntimeToken(config);
|
|
181
|
+
const runtime = Boolean(runtimeToken);
|
|
182
|
+
return parseResponse(await fetcher(tableRowsUrl(runtime), {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: await headers(runtimeToken),
|
|
185
|
+
body: JSON.stringify(withTenant(data, runtime))
|
|
186
|
+
}));
|
|
187
|
+
},
|
|
188
|
+
async update(id, data) {
|
|
189
|
+
const runtimeToken = await resolveRuntimeToken(config);
|
|
190
|
+
const runtime = Boolean(runtimeToken);
|
|
191
|
+
await assertTenantScopedRowExists(id, runtime);
|
|
192
|
+
return parseResponse(await fetcher(tableRowsUrl(runtime), {
|
|
193
|
+
method: "PATCH",
|
|
194
|
+
headers: await headers(runtimeToken),
|
|
195
|
+
body: JSON.stringify({ primaryKeys: primaryKeys(id), data })
|
|
196
|
+
}));
|
|
197
|
+
},
|
|
198
|
+
async remove(id) {
|
|
199
|
+
const runtimeToken = await resolveRuntimeToken(config);
|
|
200
|
+
const runtime = Boolean(runtimeToken);
|
|
201
|
+
await assertTenantScopedRowExists(id, runtime);
|
|
202
|
+
await parseResponse(await fetcher(tableRowsUrl(runtime), {
|
|
203
|
+
method: "DELETE",
|
|
204
|
+
headers: await headers(runtimeToken),
|
|
205
|
+
body: JSON.stringify({ rows: [primaryKeys(id)] })
|
|
206
|
+
}));
|
|
207
|
+
},
|
|
208
|
+
async removeMany(ids) {
|
|
209
|
+
if (!ids.length) return;
|
|
210
|
+
const runtimeToken = await resolveRuntimeToken(config);
|
|
211
|
+
const runtime = Boolean(runtimeToken);
|
|
212
|
+
for (const id of ids) await assertTenantScopedRowExists(id, runtime);
|
|
213
|
+
await parseResponse(await fetcher(tableRowsUrl(runtime), {
|
|
214
|
+
method: "DELETE",
|
|
215
|
+
headers: await headers(runtimeToken),
|
|
216
|
+
body: JSON.stringify({ rows: ids.map((id) => primaryKeys(id)) })
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/data/mock.ts
|
|
223
|
+
function createMockProvider(entityDefOrSearchKeys, initialData = []) {
|
|
224
|
+
let items = [...initialData];
|
|
225
|
+
const searchableKeys = Array.isArray(entityDefOrSearchKeys) ? entityDefOrSearchKeys : entityDefOrSearchKeys.fields.filter((f) => f.searchable).map((f) => f.key);
|
|
226
|
+
const defaultSort = Array.isArray(entityDefOrSearchKeys) ? void 0 : entityDefOrSearchKeys.defaultSort;
|
|
227
|
+
const defaultSortDir = Array.isArray(entityDefOrSearchKeys) ? "asc" : entityDefOrSearchKeys.defaultSortDir ?? "asc";
|
|
228
|
+
return {
|
|
229
|
+
async list(query) {
|
|
230
|
+
let filtered = [...items];
|
|
231
|
+
if (query.search && searchableKeys.length > 0) {
|
|
232
|
+
const term = query.search.toLowerCase();
|
|
233
|
+
filtered = filtered.filter(
|
|
234
|
+
(item) => searchableKeys.some((key) => {
|
|
235
|
+
const val = item[key];
|
|
236
|
+
return typeof val === "string" && val.toLowerCase().includes(term);
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (query.filters) {
|
|
241
|
+
for (const [key, val] of Object.entries(query.filters)) {
|
|
242
|
+
if (val == null || val === "") continue;
|
|
243
|
+
const range = parseRangeFilter(val);
|
|
244
|
+
filtered = range ? filtered.filter((item) => matchesRange(item[key], range)) : filtered.filter((item) => item[key] === val);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const sortBy = query.sortBy ?? defaultSort;
|
|
248
|
+
const sortDir = query.sortDir ?? defaultSortDir;
|
|
249
|
+
if (sortBy) {
|
|
250
|
+
filtered.sort((a, b) => {
|
|
251
|
+
const av = a[sortBy] ?? "";
|
|
252
|
+
const bv = b[sortBy] ?? "";
|
|
253
|
+
const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av).localeCompare(String(bv));
|
|
254
|
+
return sortDir === "desc" ? -cmp : cmp;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const total = filtered.length;
|
|
258
|
+
const page = query.page ?? 1;
|
|
259
|
+
const pageSize = query.pageSize ?? 50;
|
|
260
|
+
const start = (page - 1) * pageSize;
|
|
261
|
+
const data = filtered.slice(start, start + pageSize);
|
|
262
|
+
return { data, total };
|
|
263
|
+
},
|
|
264
|
+
async create(data) {
|
|
265
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
266
|
+
const item = {
|
|
267
|
+
...data,
|
|
268
|
+
id: crypto.randomUUID(),
|
|
269
|
+
tenantId: "mock-tenant",
|
|
270
|
+
createdAt: now,
|
|
271
|
+
updatedAt: now
|
|
272
|
+
};
|
|
273
|
+
items.unshift(item);
|
|
274
|
+
return item;
|
|
275
|
+
},
|
|
276
|
+
async update(id, data) {
|
|
277
|
+
const idx = items.findIndex((i) => i.id === id);
|
|
278
|
+
if (idx === -1) throw new Error(`[@fayz-ai/core] Mock provider: item not found: ${id}`);
|
|
279
|
+
items[idx] = { ...items[idx], ...data, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
280
|
+
return items[idx];
|
|
281
|
+
},
|
|
282
|
+
async remove(id) {
|
|
283
|
+
items = items.filter((i) => i.id !== id);
|
|
284
|
+
},
|
|
285
|
+
async removeMany(ids) {
|
|
286
|
+
const drop = new Set(ids);
|
|
287
|
+
items = items.filter((i) => !drop.has(i.id));
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/data/archetype.ts
|
|
293
|
+
var HAS_KIND = /* @__PURE__ */ new Set(["person", "category", "order", "transaction", "schedule", "location"]);
|
|
294
|
+
var ARCHETYPE_CONFIG = {
|
|
295
|
+
person: { table: "people", fkColumn: "person_id" },
|
|
296
|
+
category: { table: "categories", fkColumn: "category_id" },
|
|
297
|
+
product: { table: "products", fkColumn: "product_id" },
|
|
298
|
+
service: { table: "services", fkColumn: "service_id" },
|
|
299
|
+
location: { table: "locations", fkColumn: "location_id" },
|
|
300
|
+
order: { table: "orders", fkColumn: "order_id" },
|
|
301
|
+
transaction: { table: "transactions", fkColumn: "transaction_id" },
|
|
302
|
+
schedule: { table: "schedules", fkColumn: "schedule_id" }
|
|
303
|
+
};
|
|
304
|
+
var CUSTOM_FIELDS_COLUMN = "custom_fields";
|
|
305
|
+
var CUSTOM_FIELD_FILTER_PREFIX = "custom.";
|
|
306
|
+
var ARCHETYPE_COLUMNS = {
|
|
307
|
+
person: /* @__PURE__ */ new Set(["name", "email", "phone", "document_number", "avatar_url", "date_of_birth", "address", "city", "state", "country", "postal_code", "tags", "is_active", "notes", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
308
|
+
category: /* @__PURE__ */ new Set(["name", "slug", "parent_id", "icon", "color", "sort_order", "is_active", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
309
|
+
product: /* @__PURE__ */ new Set(["name", "description", "sku", "price", "cost", "currency", "unit", "image_url", "stock", "min_stock", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN]),
|
|
310
|
+
service: /* @__PURE__ */ new Set(["name", "description", "price", "cost", "currency", "duration_minutes", "image_url", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN]),
|
|
311
|
+
location: /* @__PURE__ */ new Set(["name", "email", "phone", "address", "city", "state", "country", "postal_code", "is_headquarters", "is_active", "tags", "notes", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
312
|
+
order: /* @__PURE__ */ new Set(["reference_number", "status", "party_id", "assignee_id", "location_id", "subtotal", "discount", "tax", "total", "currency", "due_at", "completed_at", "notes", "tags", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
313
|
+
transaction: /* @__PURE__ */ new Set(["order_id", "party_id", "amount", "currency", "payment_method", "reference", "status", "transacted_at", "notes", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
314
|
+
schedule: /* @__PURE__ */ new Set(["assignee_id", "location_id", "day_of_week", "specific_date", "starts_at", "ends_at", "is_active", "metadata", CUSTOM_FIELDS_COLUMN])
|
|
315
|
+
};
|
|
316
|
+
function camelToSnake(str) {
|
|
317
|
+
return str.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
|
|
318
|
+
}
|
|
319
|
+
function snakeToCamel(str) {
|
|
320
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
321
|
+
}
|
|
322
|
+
function mapRow(row) {
|
|
323
|
+
const result = {};
|
|
324
|
+
for (const [key, value] of Object.entries(row)) {
|
|
325
|
+
result[snakeToCamel(key)] = value;
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
function createArchetypeProvider(config) {
|
|
330
|
+
const ac = ARCHETYPE_CONFIG[config.archetype];
|
|
331
|
+
const archetypeColumns = ARCHETYPE_COLUMNS[config.archetype];
|
|
332
|
+
const { fkColumn } = ac;
|
|
333
|
+
function getClient2() {
|
|
334
|
+
const supabase = getSupabaseClientOptional();
|
|
335
|
+
if (!supabase) throw new Error("Supabase client not available");
|
|
336
|
+
return supabase;
|
|
337
|
+
}
|
|
338
|
+
function coreClient() {
|
|
339
|
+
return getClient2();
|
|
340
|
+
}
|
|
341
|
+
function splitFields(data) {
|
|
342
|
+
const archetypeData = {};
|
|
343
|
+
const projectData = {};
|
|
344
|
+
for (const [key, value] of Object.entries(data)) {
|
|
345
|
+
if (["id", "createdAt", "updatedAt", "createdBy", "tenantId", fkColumn].includes(key)) continue;
|
|
346
|
+
const snakeKey = camelToSnake(key);
|
|
347
|
+
if (archetypeColumns.has(snakeKey)) {
|
|
348
|
+
archetypeData[snakeKey] = value;
|
|
349
|
+
} else {
|
|
350
|
+
projectData[snakeKey] = value;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { archetypeData, projectData };
|
|
354
|
+
}
|
|
355
|
+
const VIEW_MAP = {
|
|
356
|
+
clients: "v_clients",
|
|
357
|
+
staff_members: "v_staff"
|
|
358
|
+
};
|
|
359
|
+
const viewName = VIEW_MAP[config.projectTable] ?? `v_${config.projectTable}`;
|
|
360
|
+
const isPure = config.projectTable === ac.table;
|
|
361
|
+
return {
|
|
362
|
+
async list(query) {
|
|
363
|
+
const tenantId = config.tenantId();
|
|
364
|
+
if (!tenantId) return { data: [], total: 0 };
|
|
365
|
+
const countOpts = query.countMode === "none" ? void 0 : { count: "exact" };
|
|
366
|
+
let q = isPure ? coreClient().from(ac.table).select("*", countOpts).eq("tenant_id", tenantId) : getClient2().from(viewName).select("*", countOpts).eq("tenant_id", tenantId);
|
|
367
|
+
if (isPure && HAS_KIND.has(config.archetype)) {
|
|
368
|
+
q = q.eq("kind", config.archetypeKind);
|
|
369
|
+
}
|
|
370
|
+
if (query.filters) {
|
|
371
|
+
for (const [key, val] of Object.entries(query.filters)) {
|
|
372
|
+
if (val == null || val === "") continue;
|
|
373
|
+
const col = key.startsWith(CUSTOM_FIELD_FILTER_PREFIX) ? `${CUSTOM_FIELDS_COLUMN}->>${key.slice(CUSTOM_FIELD_FILTER_PREFIX.length)}` : camelToSnake(key);
|
|
374
|
+
const range = parseRangeFilter(val);
|
|
375
|
+
if (range) {
|
|
376
|
+
q = applyRangeToQuery(q, col, range);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (isFilterObject(val)) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`[@fayz-ai/core] Filter "${key}" is an object with no recognised bound. Use {from,to} / {gte,lte,gt,lt} for ranges, or a plain value for equality.`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
q = q.eq(col, val);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (query.search && config.searchColumns && config.searchColumns.length > 0) {
|
|
388
|
+
const term = `%${query.search}%`;
|
|
389
|
+
const orClauses = config.searchColumns.map((col) => camelToSnake(col)).map((col) => `${col}.ilike.${term}`);
|
|
390
|
+
if (orClauses.length > 0) {
|
|
391
|
+
q = q.or(orClauses.join(","));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (query.sortBy) {
|
|
395
|
+
q = q.order(camelToSnake(query.sortBy), { ascending: query.sortDir !== "desc" });
|
|
396
|
+
} else {
|
|
397
|
+
q = q.order("created_at", { ascending: false });
|
|
398
|
+
}
|
|
399
|
+
const page = query.page ?? 1;
|
|
400
|
+
const pageSize = query.pageSize ?? 50;
|
|
401
|
+
const from = (page - 1) * pageSize;
|
|
402
|
+
q = q.range(from, from + pageSize - 1);
|
|
403
|
+
const { data, error, count } = await q;
|
|
404
|
+
if (error) throw error;
|
|
405
|
+
const rows = (data ?? []).map((row) => mapRow(row));
|
|
406
|
+
return { data: rows, total: count ?? 0 };
|
|
407
|
+
},
|
|
408
|
+
async create(data) {
|
|
409
|
+
const tenantId = config.tenantId();
|
|
410
|
+
if (!tenantId) throw new Error("No tenant selected");
|
|
411
|
+
const { archetypeData, projectData } = splitFields(data);
|
|
412
|
+
archetypeData.tenant_id = tenantId;
|
|
413
|
+
if (HAS_KIND.has(config.archetype)) {
|
|
414
|
+
archetypeData.kind = config.archetypeKind;
|
|
415
|
+
}
|
|
416
|
+
const { data: archetypeRow, error: archetypeError } = await coreClient().from(ac.table).insert(archetypeData).select().single();
|
|
417
|
+
if (archetypeError) throw archetypeError;
|
|
418
|
+
const archetypeId = archetypeRow.id;
|
|
419
|
+
if (!isPure) {
|
|
420
|
+
projectData[fkColumn] = archetypeId;
|
|
421
|
+
projectData.tenant_id = tenantId;
|
|
422
|
+
const { error: projectError } = await getClient2().from(config.projectTable).insert(projectData);
|
|
423
|
+
if (projectError) {
|
|
424
|
+
await coreClient().from(ac.table).delete().eq("id", archetypeId);
|
|
425
|
+
throw projectError;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { id: archetypeId, ...archetypeRow, ...isPure ? {} : projectData };
|
|
429
|
+
},
|
|
430
|
+
async update(id, data) {
|
|
431
|
+
const { archetypeData, projectData } = splitFields(data);
|
|
432
|
+
if (Object.keys(archetypeData).length > 0) {
|
|
433
|
+
const { error } = await coreClient().from(ac.table).update(archetypeData).eq("id", id);
|
|
434
|
+
if (error) throw error;
|
|
435
|
+
}
|
|
436
|
+
if (!isPure && Object.keys(projectData).length > 0) {
|
|
437
|
+
const { error } = await getClient2().from(config.projectTable).update(projectData).eq(fkColumn, id);
|
|
438
|
+
if (error) throw error;
|
|
439
|
+
}
|
|
440
|
+
const { data: archetypeRow } = await coreClient().from(ac.table).select("*").eq("id", id).single();
|
|
441
|
+
if (isPure) {
|
|
442
|
+
return mapRow({ ...archetypeRow, id });
|
|
443
|
+
}
|
|
444
|
+
const { data: projectRow } = await getClient2().from(config.projectTable).select("*").eq(fkColumn, id).single();
|
|
445
|
+
const flat = { ...archetypeRow, ...projectRow, id };
|
|
446
|
+
if (archetypeRow && CUSTOM_FIELDS_COLUMN in archetypeRow) {
|
|
447
|
+
flat[CUSTOM_FIELDS_COLUMN] = archetypeRow[CUSTOM_FIELDS_COLUMN];
|
|
448
|
+
}
|
|
449
|
+
delete flat[fkColumn];
|
|
450
|
+
return mapRow(flat);
|
|
451
|
+
},
|
|
452
|
+
async remove(id) {
|
|
453
|
+
const { error } = await coreClient().from(ac.table).delete().eq("id", id);
|
|
454
|
+
if (error) throw error;
|
|
455
|
+
},
|
|
456
|
+
async removeMany(ids) {
|
|
457
|
+
if (!ids.length) return;
|
|
458
|
+
const { error } = await coreClient().from(ac.table).delete().in("id", ids);
|
|
459
|
+
if (error) throw error;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/data/cached.ts
|
|
465
|
+
function cachePrefixFor(tenantId, table) {
|
|
466
|
+
return `${tenantId ?? "_"}:${table}`;
|
|
467
|
+
}
|
|
468
|
+
function cacheKeyFor(tenantId, table, query) {
|
|
469
|
+
return `${cachePrefixFor(tenantId, table)}:${stableKey(query)}`;
|
|
470
|
+
}
|
|
471
|
+
function withCache(provider, options) {
|
|
472
|
+
function prefix() {
|
|
473
|
+
return cachePrefixFor(options.tenantId(), options.table);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
async list(query) {
|
|
477
|
+
const key = cacheKeyFor(options.tenantId(), options.table, query);
|
|
478
|
+
const cached = globalCache.get(key);
|
|
479
|
+
if (cached) return { ...cached, cached: true };
|
|
480
|
+
const result = await provider.list(query);
|
|
481
|
+
globalCache.set(key, result, options.ttl);
|
|
482
|
+
return result;
|
|
483
|
+
},
|
|
484
|
+
async create(data) {
|
|
485
|
+
const result = await provider.create(data);
|
|
486
|
+
globalCache.invalidate(prefix());
|
|
487
|
+
return result;
|
|
488
|
+
},
|
|
489
|
+
// Forwarded, not dropped: an optional method that disappears behind the
|
|
490
|
+
// wrapper silently downgrades every bulk write to a loop of single writes,
|
|
491
|
+
// and nothing upstream can tell — `createMany` was doing exactly that.
|
|
492
|
+
...provider.createMany && {
|
|
493
|
+
async createMany(rows, options2) {
|
|
494
|
+
const result = await provider.createMany(rows, options2);
|
|
495
|
+
globalCache.invalidate(prefix());
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
async update(id, data) {
|
|
500
|
+
const result = await provider.update(id, data);
|
|
501
|
+
globalCache.invalidate(prefix());
|
|
502
|
+
return result;
|
|
503
|
+
},
|
|
504
|
+
async remove(id) {
|
|
505
|
+
await provider.remove(id);
|
|
506
|
+
globalCache.invalidate(prefix());
|
|
507
|
+
},
|
|
508
|
+
...provider.removeMany && {
|
|
509
|
+
async removeMany(ids) {
|
|
510
|
+
await provider.removeMany(ids);
|
|
511
|
+
globalCache.invalidate(prefix());
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/data/errors.ts
|
|
518
|
+
var RETRYABLE_KINDS = ["offline", "server"];
|
|
519
|
+
var DataError = class extends Error {
|
|
520
|
+
constructor(kind, message, options = {}) {
|
|
521
|
+
super(message);
|
|
522
|
+
this.name = "DataError";
|
|
523
|
+
this.kind = kind;
|
|
524
|
+
this.retryable = RETRYABLE_KINDS.includes(kind);
|
|
525
|
+
this.cause = options.cause;
|
|
526
|
+
this.status = options.status;
|
|
527
|
+
this.code = options.code;
|
|
528
|
+
this.details = options.details;
|
|
529
|
+
this.hint = options.hint;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
var CODE_KINDS = {
|
|
533
|
+
// PostgREST
|
|
534
|
+
PGRST100: "invalid",
|
|
535
|
+
// parse error in the query string
|
|
536
|
+
PGRST106: "missing-object",
|
|
537
|
+
// schema not exposed
|
|
538
|
+
PGRST116: "not-found",
|
|
539
|
+
// zero rows where exactly one was requested
|
|
540
|
+
PGRST202: "missing-object",
|
|
541
|
+
// function not found in schema cache
|
|
542
|
+
PGRST204: "missing-object",
|
|
543
|
+
// column not found in schema cache
|
|
544
|
+
PGRST205: "missing-object",
|
|
545
|
+
// table not found in schema cache
|
|
546
|
+
PGRST301: "unauthorized",
|
|
547
|
+
// JWT invalid or expired
|
|
548
|
+
PGRST302: "unauthorized",
|
|
549
|
+
// anonymous access disabled
|
|
550
|
+
// Postgres
|
|
551
|
+
"22007": "invalid",
|
|
552
|
+
// invalid datetime format
|
|
553
|
+
"22P02": "invalid",
|
|
554
|
+
// invalid input syntax
|
|
555
|
+
"23502": "invalid",
|
|
556
|
+
// not-null violation
|
|
557
|
+
"23505": "conflict",
|
|
558
|
+
// unique violation
|
|
559
|
+
"23514": "invalid",
|
|
560
|
+
// check violation
|
|
561
|
+
"42501": "forbidden",
|
|
562
|
+
// insufficient privilege / permission denied
|
|
563
|
+
"42883": "missing-object",
|
|
564
|
+
// undefined function
|
|
565
|
+
"42P01": "missing-object",
|
|
566
|
+
// undefined table
|
|
567
|
+
"53300": "server",
|
|
568
|
+
// too many connections
|
|
569
|
+
"57014": "server"
|
|
570
|
+
// statement timeout, server side
|
|
571
|
+
};
|
|
572
|
+
var OFFLINE_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "ETIMEDOUT", "EAI_AGAIN"]);
|
|
573
|
+
var OFFLINE_TEXT = /failed to fetch|fetch failed|network ?error|networkrequestfailed|load failed|connection (refused|reset|closed)|err_internet_disconnected|aborted|signal is aborted|the operation was aborted|timed out|timeout/i;
|
|
574
|
+
function statusKind(status) {
|
|
575
|
+
if (status === 401) return "unauthorized";
|
|
576
|
+
if (status === 403) return "forbidden";
|
|
577
|
+
if (status === 404 || status === 406) return "not-found";
|
|
578
|
+
if (status === 408) return "offline";
|
|
579
|
+
if (status === 409) return "conflict";
|
|
580
|
+
if (status === 400 || status === 422) return "invalid";
|
|
581
|
+
if (status >= 500) return "server";
|
|
582
|
+
return "server";
|
|
583
|
+
}
|
|
584
|
+
function textKind(message) {
|
|
585
|
+
const m = message.toLowerCase();
|
|
586
|
+
if (/permission denied|row-level security|row level security|insufficient privilege/.test(m)) return "forbidden";
|
|
587
|
+
if (/does not exist|undefined table|undefined function|schema cache|could not find the (table|function)/.test(m)) return "missing-object";
|
|
588
|
+
if (/jwt|not authenticated|invalid token|unauthorized|no api key/.test(m)) return "unauthorized";
|
|
589
|
+
if (/duplicate key|already exists|conflict/.test(m)) return "conflict";
|
|
590
|
+
if (/invalid input (syntax|value)|violates (not-null|check) constraint|invalid request/.test(m)) return "invalid";
|
|
591
|
+
if (/no rows|not found/.test(m)) return "not-found";
|
|
592
|
+
return void 0;
|
|
593
|
+
}
|
|
594
|
+
function pick(source, key) {
|
|
595
|
+
const value = source[key];
|
|
596
|
+
return value === null ? void 0 : value;
|
|
597
|
+
}
|
|
598
|
+
function readStatus(source) {
|
|
599
|
+
for (const key of ["status", "statusCode", "httpStatus"]) {
|
|
600
|
+
const value = pick(source, key);
|
|
601
|
+
if (typeof value === "number") return value;
|
|
602
|
+
if (typeof value === "string" && /^\d{3}$/.test(value)) return Number(value);
|
|
603
|
+
}
|
|
604
|
+
const response = pick(source, "response");
|
|
605
|
+
if (response && typeof response === "object") {
|
|
606
|
+
const status = response.status;
|
|
607
|
+
if (typeof status === "number") return status;
|
|
608
|
+
}
|
|
609
|
+
return void 0;
|
|
610
|
+
}
|
|
611
|
+
function isOffline(name, message, code, status) {
|
|
612
|
+
if (name === "AbortError" || name === "TimeoutError") return true;
|
|
613
|
+
if (code && OFFLINE_CODES.has(code)) return true;
|
|
614
|
+
if (status !== void 0) return false;
|
|
615
|
+
const nav = globalThis.navigator;
|
|
616
|
+
if (nav && nav.onLine === false) return true;
|
|
617
|
+
return OFFLINE_TEXT.test(message);
|
|
618
|
+
}
|
|
619
|
+
function toDataError(err) {
|
|
620
|
+
if (err instanceof DataError) return err;
|
|
621
|
+
const source = err && typeof err === "object" ? err : {};
|
|
622
|
+
const name = typeof source.name === "string" ? source.name : "";
|
|
623
|
+
const rawMessage = typeof source.message === "string" ? source.message : "";
|
|
624
|
+
const message = rawMessage || (typeof err === "string" ? err : "") || "data request failed";
|
|
625
|
+
const codeValue = pick(source, "code");
|
|
626
|
+
const code = typeof codeValue === "string" ? codeValue : typeof codeValue === "number" ? String(codeValue) : void 0;
|
|
627
|
+
const status = readStatus(source);
|
|
628
|
+
const details = typeof source.details === "string" ? source.details : void 0;
|
|
629
|
+
const hint = typeof source.hint === "string" ? source.hint : void 0;
|
|
630
|
+
let kind = code ? CODE_KINDS[code] : void 0;
|
|
631
|
+
if (!kind && isOffline(name, message, code, status)) kind = "offline";
|
|
632
|
+
if (!kind && status !== void 0) kind = statusKind(status);
|
|
633
|
+
if (!kind) kind = textKind(message);
|
|
634
|
+
return new DataError(kind ?? "server", message, { cause: err, status, code, details, hint });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/events/bus.ts
|
|
638
|
+
function createEventBus() {
|
|
639
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
640
|
+
const bus = {
|
|
641
|
+
emit(event, payload) {
|
|
642
|
+
const set = handlers.get(event);
|
|
643
|
+
if (!set) return;
|
|
644
|
+
for (const handler of Array.from(set)) {
|
|
645
|
+
try {
|
|
646
|
+
handler(payload);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
console.error(`[fayz] event handler for "${event}" threw:`, err);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
on(event, handler) {
|
|
653
|
+
let set = handlers.get(event);
|
|
654
|
+
if (!set) {
|
|
655
|
+
set = /* @__PURE__ */ new Set();
|
|
656
|
+
handlers.set(event, set);
|
|
657
|
+
}
|
|
658
|
+
set.add(handler);
|
|
659
|
+
return () => bus.off(event, handler);
|
|
660
|
+
},
|
|
661
|
+
once(event, handler) {
|
|
662
|
+
const wrapped = (payload) => {
|
|
663
|
+
bus.off(event, wrapped);
|
|
664
|
+
handler(payload);
|
|
665
|
+
};
|
|
666
|
+
return bus.on(event, wrapped);
|
|
667
|
+
},
|
|
668
|
+
off(event, handler) {
|
|
669
|
+
handlers.get(event)?.delete(handler);
|
|
670
|
+
},
|
|
671
|
+
clear(event) {
|
|
672
|
+
if (event) handlers.delete(event);
|
|
673
|
+
else handlers.clear();
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
return bus;
|
|
677
|
+
}
|
|
678
|
+
var eventBus = createEventBus();
|
|
679
|
+
var DATA_CHANGED_EVENT = "data:changed";
|
|
680
|
+
function matchesDataChange(match, payload) {
|
|
681
|
+
const attributed = payload.table ?? payload.entityKey ?? payload.archetype;
|
|
682
|
+
if (!attributed) return true;
|
|
683
|
+
if (match.table && payload.table && match.table === payload.table) return true;
|
|
684
|
+
if (match.entityKey && payload.entityKey && match.entityKey === payload.entityKey) return true;
|
|
685
|
+
if (match.archetype && payload.archetype && match.archetype === payload.archetype) return true;
|
|
686
|
+
return !match.table && !match.entityKey && !match.archetype;
|
|
687
|
+
}
|
|
688
|
+
var pending = [];
|
|
689
|
+
var flushTimer = null;
|
|
690
|
+
function flush() {
|
|
691
|
+
const batch = pending;
|
|
692
|
+
pending = [];
|
|
693
|
+
flushTimer = null;
|
|
694
|
+
for (const payload of batch) eventBus.emit(DATA_CHANGED_EVENT, payload);
|
|
695
|
+
}
|
|
696
|
+
function emitDataChanged(payload = {}) {
|
|
697
|
+
if (payload.table) {
|
|
698
|
+
const tenant = getActiveTenantId() ?? "_";
|
|
699
|
+
const sibling = payload.table.startsWith("v_") ? payload.table.slice(2) : `v_${payload.table}`;
|
|
700
|
+
globalCache.invalidate(`${tenant}:${payload.table}`);
|
|
701
|
+
globalCache.invalidate(`${tenant}:${sibling}`);
|
|
702
|
+
} else {
|
|
703
|
+
globalCache.clear();
|
|
704
|
+
}
|
|
705
|
+
pending.push({ op: "unknown", source: "agent", ...payload });
|
|
706
|
+
if (flushTimer) return;
|
|
707
|
+
flushTimer = setTimeout(flush, 120);
|
|
708
|
+
}
|
|
709
|
+
function useDataChanged(match, handler, deps = []) {
|
|
710
|
+
const handlerRef = React.useRef(handler);
|
|
711
|
+
handlerRef.current = handler;
|
|
712
|
+
const { table, entityKey, archetype } = match;
|
|
713
|
+
React.useEffect(
|
|
714
|
+
() => eventBus.on(DATA_CHANGED_EVENT, (payload) => {
|
|
715
|
+
if (matchesDataChange({ table, entityKey, archetype }, payload)) handlerRef.current(payload);
|
|
716
|
+
}),
|
|
717
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
718
|
+
[table, entityKey, archetype, ...deps]
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/lib/cache-idb.ts
|
|
723
|
+
var DB_VERSION = 1;
|
|
724
|
+
var STORE_ENTRIES = "entries";
|
|
725
|
+
var STORE_REPLICAS = "replicas";
|
|
726
|
+
function idbAvailable() {
|
|
727
|
+
return typeof indexedDB !== "undefined" && typeof IDBKeyRange !== "undefined";
|
|
728
|
+
}
|
|
729
|
+
var NOOP_CACHE = {
|
|
730
|
+
available: false,
|
|
731
|
+
hydrate: async () => 0,
|
|
732
|
+
getStale: async () => void 0,
|
|
733
|
+
put: () => {
|
|
734
|
+
},
|
|
735
|
+
expire: () => {
|
|
736
|
+
},
|
|
737
|
+
purge: () => {
|
|
738
|
+
},
|
|
739
|
+
putReplica: () => {
|
|
740
|
+
},
|
|
741
|
+
getReplica: async () => void 0,
|
|
742
|
+
dropReplica: () => {
|
|
743
|
+
},
|
|
744
|
+
flush: async () => {
|
|
745
|
+
},
|
|
746
|
+
detach: () => {
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
var attached = null;
|
|
750
|
+
function getPersistentCache() {
|
|
751
|
+
return attached;
|
|
752
|
+
}
|
|
753
|
+
function attachPersistentCache(store = globalCache, options = {}) {
|
|
754
|
+
attached?.detach();
|
|
755
|
+
if (!idbAvailable()) {
|
|
756
|
+
attached = NOOP_CACHE;
|
|
757
|
+
return NOOP_CACHE;
|
|
758
|
+
}
|
|
759
|
+
const dbName = options.dbName ?? "core-cache";
|
|
760
|
+
const maxAgeMs = options.maxAgeMs ?? 24 * 60 * 60 * 1e3;
|
|
761
|
+
const flushDelayMs = options.flushDelayMs ?? 50;
|
|
762
|
+
const defaultTTL = options.defaultTTL ?? DEFAULT_CACHE_TTL;
|
|
763
|
+
let dbPromise = null;
|
|
764
|
+
function openDb() {
|
|
765
|
+
if (dbPromise) return dbPromise;
|
|
766
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
767
|
+
const request = indexedDB.open(dbName, DB_VERSION);
|
|
768
|
+
request.onupgradeneeded = () => {
|
|
769
|
+
const db = request.result;
|
|
770
|
+
if (!db.objectStoreNames.contains(STORE_ENTRIES)) db.createObjectStore(STORE_ENTRIES);
|
|
771
|
+
if (!db.objectStoreNames.contains(STORE_REPLICAS)) db.createObjectStore(STORE_REPLICAS);
|
|
772
|
+
};
|
|
773
|
+
request.onsuccess = () => resolve(request.result);
|
|
774
|
+
request.onerror = () => reject(request.error ?? new Error("cache database failed to open"));
|
|
775
|
+
request.onblocked = () => reject(new Error("cache database blocked by another tab"));
|
|
776
|
+
});
|
|
777
|
+
return dbPromise;
|
|
778
|
+
}
|
|
779
|
+
function promisify(request) {
|
|
780
|
+
return new Promise((resolve, reject) => {
|
|
781
|
+
request.onsuccess = () => resolve(request.result);
|
|
782
|
+
request.onerror = () => reject(request.error ?? new Error("cache request failed"));
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
async function transact(names, mode, run) {
|
|
786
|
+
const db = await openDb();
|
|
787
|
+
const tx = db.transaction(names, mode);
|
|
788
|
+
const done = new Promise((resolve, reject) => {
|
|
789
|
+
tx.oncomplete = () => resolve();
|
|
790
|
+
tx.onerror = () => reject(tx.error ?? new Error("cache transaction failed"));
|
|
791
|
+
tx.onabort = () => reject(tx.error ?? new Error("cache transaction aborted"));
|
|
792
|
+
});
|
|
793
|
+
const value = await run(tx);
|
|
794
|
+
await done;
|
|
795
|
+
return value;
|
|
796
|
+
}
|
|
797
|
+
let queue = [];
|
|
798
|
+
let timer = null;
|
|
799
|
+
let running = Promise.resolve();
|
|
800
|
+
let detached = false;
|
|
801
|
+
function enqueue(op) {
|
|
802
|
+
if (detached) return;
|
|
803
|
+
if (op.op === "put") {
|
|
804
|
+
let barrier = -1;
|
|
805
|
+
for (let i = queue.length - 1; i >= 0; i--) {
|
|
806
|
+
if (queue[i].op !== "put") {
|
|
807
|
+
barrier = i;
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const at = queue.findIndex(
|
|
812
|
+
(q, i) => i > barrier && q.op === "put" && q.store === op.store && q.key === op.key
|
|
813
|
+
);
|
|
814
|
+
if (at >= 0) queue[at] = op;
|
|
815
|
+
else queue.push(op);
|
|
816
|
+
} else if (op.op === "purge") {
|
|
817
|
+
queue = queue.filter((q) => !(q.op !== "expire" && q.store === op.store && "key" in q && q.key.startsWith(op.prefix)));
|
|
818
|
+
queue.push(op);
|
|
819
|
+
} else {
|
|
820
|
+
queue.push(op);
|
|
821
|
+
}
|
|
822
|
+
schedule();
|
|
823
|
+
}
|
|
824
|
+
function schedule() {
|
|
825
|
+
if (timer || detached) return;
|
|
826
|
+
timer = setTimeout(() => {
|
|
827
|
+
timer = null;
|
|
828
|
+
running = running.then(drain);
|
|
829
|
+
}, flushDelayMs);
|
|
830
|
+
}
|
|
831
|
+
async function drain() {
|
|
832
|
+
const batch = queue;
|
|
833
|
+
queue = [];
|
|
834
|
+
if (batch.length === 0) return;
|
|
835
|
+
try {
|
|
836
|
+
let segment = [];
|
|
837
|
+
for (const op of batch) {
|
|
838
|
+
if (op.op === "expire") {
|
|
839
|
+
if (segment.length) await commit(segment);
|
|
840
|
+
segment = [];
|
|
841
|
+
await applyExpiry(op.prefix);
|
|
842
|
+
} else {
|
|
843
|
+
segment.push(op);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (segment.length) await commit(segment);
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function commit(batch) {
|
|
851
|
+
await transact([STORE_ENTRIES, STORE_REPLICAS], "readwrite", async (tx) => {
|
|
852
|
+
for (const op of batch) {
|
|
853
|
+
if (op.op === "expire") continue;
|
|
854
|
+
const objectStore = tx.objectStore(op.store);
|
|
855
|
+
try {
|
|
856
|
+
if (op.op === "put") objectStore.put(op.value, op.key);
|
|
857
|
+
else if (op.op === "delete") objectStore.delete(op.key);
|
|
858
|
+
else objectStore.delete(rangeFor(op.prefix));
|
|
859
|
+
} catch {
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
async function applyExpiry(prefix) {
|
|
865
|
+
await transact([STORE_ENTRIES], "readwrite", async (tx) => {
|
|
866
|
+
const objectStore = tx.objectStore(STORE_ENTRIES);
|
|
867
|
+
const keysPromise = promisify(objectStore.getAllKeys());
|
|
868
|
+
const valuesPromise = promisify(objectStore.getAll());
|
|
869
|
+
const keys = await keysPromise;
|
|
870
|
+
const values = await valuesPromise;
|
|
871
|
+
for (let i = 0; i < keys.length; i++) {
|
|
872
|
+
const key = String(keys[i]);
|
|
873
|
+
const value = values[i];
|
|
874
|
+
if (!value || prefix && !key.startsWith(prefix)) continue;
|
|
875
|
+
if (value.expiresAt === 0) continue;
|
|
876
|
+
objectStore.put({ ...value, expiresAt: 0 }, key);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async function flush2() {
|
|
881
|
+
for (let round = 0; round < 8; round++) {
|
|
882
|
+
if (timer) {
|
|
883
|
+
clearTimeout(timer);
|
|
884
|
+
timer = null;
|
|
885
|
+
}
|
|
886
|
+
running = running.then(drain);
|
|
887
|
+
await running;
|
|
888
|
+
if (queue.length === 0 && !timer) return;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function rangeFor(prefix) {
|
|
892
|
+
return IDBKeyRange.bound(prefix, `${prefix}\uFFFF`);
|
|
893
|
+
}
|
|
894
|
+
function queuedPut(storeName, key) {
|
|
895
|
+
for (let i = queue.length - 1; i >= 0; i--) {
|
|
896
|
+
const op = queue[i];
|
|
897
|
+
if (op.op === "put" && op.store === storeName && op.key === key) return op.value;
|
|
898
|
+
}
|
|
899
|
+
return void 0;
|
|
900
|
+
}
|
|
901
|
+
const originalSet = store.set.bind(store);
|
|
902
|
+
const originalInvalidate = store.invalidate.bind(store);
|
|
903
|
+
const originalClear = store.clear.bind(store);
|
|
904
|
+
store.set = (key, data, ttl) => {
|
|
905
|
+
originalSet(key, data, ttl);
|
|
906
|
+
enqueue({
|
|
907
|
+
op: "put",
|
|
908
|
+
store: STORE_ENTRIES,
|
|
909
|
+
key,
|
|
910
|
+
value: { data, expiresAt: Date.now() + (ttl ?? defaultTTL), storedAt: Date.now() }
|
|
911
|
+
});
|
|
912
|
+
};
|
|
913
|
+
store.invalidate = (prefix) => {
|
|
914
|
+
originalInvalidate(prefix);
|
|
915
|
+
enqueue({ op: "expire", prefix });
|
|
916
|
+
};
|
|
917
|
+
store.clear = () => {
|
|
918
|
+
originalClear();
|
|
919
|
+
enqueue({ op: "expire", prefix: "" });
|
|
920
|
+
};
|
|
921
|
+
const handle = {
|
|
922
|
+
available: true,
|
|
923
|
+
async hydrate() {
|
|
924
|
+
if (detached) return 0;
|
|
925
|
+
const now = Date.now();
|
|
926
|
+
try {
|
|
927
|
+
const { keys, values } = await transact([STORE_ENTRIES], "readonly", async (tx) => {
|
|
928
|
+
const objectStore = tx.objectStore(STORE_ENTRIES);
|
|
929
|
+
const keysPromise = promisify(objectStore.getAllKeys());
|
|
930
|
+
const valuesPromise = promisify(objectStore.getAll());
|
|
931
|
+
return {
|
|
932
|
+
keys: await keysPromise,
|
|
933
|
+
values: await valuesPromise
|
|
934
|
+
};
|
|
935
|
+
});
|
|
936
|
+
let loaded = 0;
|
|
937
|
+
for (let i = 0; i < keys.length; i++) {
|
|
938
|
+
const key = String(keys[i]);
|
|
939
|
+
const entry = values[i];
|
|
940
|
+
if (!entry) continue;
|
|
941
|
+
if (now - entry.storedAt > maxAgeMs) {
|
|
942
|
+
enqueue({ op: "delete", store: STORE_ENTRIES, key });
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (entry.expiresAt > now) {
|
|
946
|
+
originalSet(key, entry.data, entry.expiresAt - now);
|
|
947
|
+
loaded++;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return loaded;
|
|
951
|
+
} catch {
|
|
952
|
+
return 0;
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
async getStale(key) {
|
|
956
|
+
if (detached) return void 0;
|
|
957
|
+
const queued = queuedPut(STORE_ENTRIES, key);
|
|
958
|
+
if (queued) return queued;
|
|
959
|
+
try {
|
|
960
|
+
const entry = await transact(
|
|
961
|
+
[STORE_ENTRIES],
|
|
962
|
+
"readonly",
|
|
963
|
+
(tx) => promisify(tx.objectStore(STORE_ENTRIES).get(key))
|
|
964
|
+
);
|
|
965
|
+
if (!entry) return void 0;
|
|
966
|
+
if (Date.now() - entry.storedAt > maxAgeMs) return void 0;
|
|
967
|
+
return entry;
|
|
968
|
+
} catch {
|
|
969
|
+
return void 0;
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
put(key, data, expiresAt) {
|
|
973
|
+
enqueue({ op: "put", store: STORE_ENTRIES, key, value: { data, expiresAt, storedAt: Date.now() } });
|
|
974
|
+
},
|
|
975
|
+
expire(prefix) {
|
|
976
|
+
enqueue({ op: "expire", prefix });
|
|
977
|
+
},
|
|
978
|
+
purge(prefix = "") {
|
|
979
|
+
enqueue({ op: "purge", store: STORE_ENTRIES, prefix });
|
|
980
|
+
enqueue({ op: "purge", store: STORE_REPLICAS, prefix });
|
|
981
|
+
},
|
|
982
|
+
putReplica(key, rows) {
|
|
983
|
+
enqueue({ op: "put", store: STORE_REPLICAS, key, value: { rows, syncedAt: Date.now() } });
|
|
984
|
+
},
|
|
985
|
+
async getReplica(key) {
|
|
986
|
+
if (detached) return void 0;
|
|
987
|
+
const queued = queuedPut(STORE_REPLICAS, key);
|
|
988
|
+
if (queued) return queued;
|
|
989
|
+
try {
|
|
990
|
+
return await transact(
|
|
991
|
+
[STORE_REPLICAS],
|
|
992
|
+
"readonly",
|
|
993
|
+
(tx) => promisify(tx.objectStore(STORE_REPLICAS).get(key))
|
|
994
|
+
);
|
|
995
|
+
} catch {
|
|
996
|
+
return void 0;
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
dropReplica(key) {
|
|
1000
|
+
enqueue({ op: "delete", store: STORE_REPLICAS, key });
|
|
1001
|
+
},
|
|
1002
|
+
flush: flush2,
|
|
1003
|
+
detach() {
|
|
1004
|
+
if (detached) return;
|
|
1005
|
+
detached = true;
|
|
1006
|
+
if (timer) {
|
|
1007
|
+
clearTimeout(timer);
|
|
1008
|
+
timer = null;
|
|
1009
|
+
}
|
|
1010
|
+
store.set = originalSet;
|
|
1011
|
+
store.invalidate = originalInvalidate;
|
|
1012
|
+
store.clear = originalClear;
|
|
1013
|
+
if (attached === handle) attached = null;
|
|
1014
|
+
void dbPromise?.then((db) => db.close()).catch(() => {
|
|
1015
|
+
});
|
|
1016
|
+
dbPromise = null;
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
if (options.requestPersistence !== false) void requestPersistence();
|
|
1020
|
+
attached = handle;
|
|
1021
|
+
return handle;
|
|
1022
|
+
}
|
|
1023
|
+
async function requestPersistence() {
|
|
1024
|
+
try {
|
|
1025
|
+
const storage = globalThis.navigator?.storage;
|
|
1026
|
+
await storage?.persist?.();
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/data/offline.ts
|
|
1032
|
+
var REPLICA_PAGE_SIZE = 500;
|
|
1033
|
+
var REPLICA_MAX_ROWS = 5e3;
|
|
1034
|
+
var REPLICA_FIRST_DELAY_MS = 1500;
|
|
1035
|
+
var REPLICA_REFRESH_DELAY_MS = 3e3;
|
|
1036
|
+
var REPLICA_MIN_INTERVAL_MS = 6e4;
|
|
1037
|
+
function replicaKeyFor(tenantId, table) {
|
|
1038
|
+
return `replica:${cachePrefixFor(tenantId, table)}`;
|
|
1039
|
+
}
|
|
1040
|
+
function withOffline(provider, options) {
|
|
1041
|
+
if (options.policy === "online") return provider;
|
|
1042
|
+
const ttl = options.ttl ?? DEFAULT_CACHE_TTL;
|
|
1043
|
+
if (options.policy === "replica") ensureReplicaSync(provider, options);
|
|
1044
|
+
async function fromPersisted(query) {
|
|
1045
|
+
const l2 = getPersistentCache();
|
|
1046
|
+
if (!l2?.available) return void 0;
|
|
1047
|
+
const entry = await l2.getStale(cacheKeyFor(options.tenantId(), options.table, query));
|
|
1048
|
+
if (!entry) return void 0;
|
|
1049
|
+
return { ...entry.data, stale: true, staleAt: entry.storedAt };
|
|
1050
|
+
}
|
|
1051
|
+
async function fromReplica(query) {
|
|
1052
|
+
const l2 = getPersistentCache();
|
|
1053
|
+
if (!l2?.available) return void 0;
|
|
1054
|
+
const snapshot = await l2.getReplica(replicaKeyFor(options.tenantId(), options.table));
|
|
1055
|
+
if (!snapshot) return void 0;
|
|
1056
|
+
const result = await localList(snapshot.rows, query, options.entityDef);
|
|
1057
|
+
return { ...result, stale: true, staleAt: snapshot.syncedAt };
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
async list(query) {
|
|
1061
|
+
try {
|
|
1062
|
+
const result = await provider.list(query);
|
|
1063
|
+
const l2 = getPersistentCache();
|
|
1064
|
+
if (options.policy === "cache" && l2?.available) {
|
|
1065
|
+
l2.put(cacheKeyFor(options.tenantId(), options.table, query), result, Date.now() + ttl);
|
|
1066
|
+
}
|
|
1067
|
+
return result;
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
const error = toDataError(err);
|
|
1070
|
+
if (error.kind !== "offline") throw error;
|
|
1071
|
+
const fallback = (options.policy === "replica" ? await fromReplica(query) : void 0) ?? await fromPersisted(query);
|
|
1072
|
+
if (!fallback) throw error;
|
|
1073
|
+
return fallback;
|
|
1074
|
+
}
|
|
1075
|
+
},
|
|
1076
|
+
async create(data) {
|
|
1077
|
+
const result = await provider.create(data);
|
|
1078
|
+
if (options.policy === "replica") scheduleReplicaSync(options, REPLICA_REFRESH_DELAY_MS);
|
|
1079
|
+
return result;
|
|
1080
|
+
},
|
|
1081
|
+
...provider.createMany && {
|
|
1082
|
+
async createMany(rows, bulkOptions) {
|
|
1083
|
+
const result = await provider.createMany(rows, bulkOptions);
|
|
1084
|
+
if (options.policy === "replica") scheduleReplicaSync(options, REPLICA_REFRESH_DELAY_MS);
|
|
1085
|
+
return result;
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1088
|
+
async update(id, data) {
|
|
1089
|
+
const result = await provider.update(id, data);
|
|
1090
|
+
if (options.policy === "replica") scheduleReplicaSync(options, REPLICA_REFRESH_DELAY_MS);
|
|
1091
|
+
return result;
|
|
1092
|
+
},
|
|
1093
|
+
async remove(id) {
|
|
1094
|
+
await provider.remove(id);
|
|
1095
|
+
if (options.policy === "replica") scheduleReplicaSync(options, REPLICA_REFRESH_DELAY_MS);
|
|
1096
|
+
},
|
|
1097
|
+
...provider.removeMany && {
|
|
1098
|
+
async removeMany(ids) {
|
|
1099
|
+
await provider.removeMany(ids);
|
|
1100
|
+
if (options.policy === "replica") scheduleReplicaSync(options, REPLICA_REFRESH_DELAY_MS);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
async function localList(rows, query, entityDef) {
|
|
1106
|
+
const shape = entityDef ?? [];
|
|
1107
|
+
return createMockProvider(shape, rows).list(query);
|
|
1108
|
+
}
|
|
1109
|
+
async function syncReplica(provider, options) {
|
|
1110
|
+
const l2 = getPersistentCache();
|
|
1111
|
+
if (!l2?.available) return 0;
|
|
1112
|
+
const pageSize = options.pageSize ?? REPLICA_PAGE_SIZE;
|
|
1113
|
+
const maxRows = options.maxRows ?? REPLICA_MAX_ROWS;
|
|
1114
|
+
const rows = [];
|
|
1115
|
+
for (let page = 1; rows.length < maxRows; page++) {
|
|
1116
|
+
const result = await provider.list({ page, pageSize, countMode: "none" });
|
|
1117
|
+
rows.push(...result.data);
|
|
1118
|
+
if (result.data.length < pageSize) break;
|
|
1119
|
+
}
|
|
1120
|
+
const snapshot = rows.slice(0, maxRows);
|
|
1121
|
+
l2.putReplica(replicaKeyFor(options.tenantId, options.table), snapshot);
|
|
1122
|
+
await l2.flush();
|
|
1123
|
+
return snapshot.length;
|
|
1124
|
+
}
|
|
1125
|
+
var replicaSyncs = /* @__PURE__ */ new Map();
|
|
1126
|
+
function ensureReplicaSync(provider, options) {
|
|
1127
|
+
const key = replicaKeyFor(options.tenantId(), options.table);
|
|
1128
|
+
if (replicaSyncs.has(key)) return;
|
|
1129
|
+
const state = {
|
|
1130
|
+
// Tenant read at sync time, not at registration: the first provider is often
|
|
1131
|
+
// built before the session settles on one.
|
|
1132
|
+
run: () => syncReplica(provider, {
|
|
1133
|
+
table: options.table,
|
|
1134
|
+
tenantId: options.tenantId(),
|
|
1135
|
+
pageSize: options.replicaPageSize,
|
|
1136
|
+
maxRows: options.replicaMaxRows
|
|
1137
|
+
}),
|
|
1138
|
+
timer: null,
|
|
1139
|
+
lastSyncAt: 0,
|
|
1140
|
+
running: false
|
|
1141
|
+
};
|
|
1142
|
+
replicaSyncs.set(key, state);
|
|
1143
|
+
const table = options.table.split("#")[0];
|
|
1144
|
+
const sibling = table.startsWith("v_") ? table.slice(2) : `v_${table}`;
|
|
1145
|
+
eventBus.on(DATA_CHANGED_EVENT, (payload) => {
|
|
1146
|
+
if (payload.table && payload.table !== table && payload.table !== sibling) return;
|
|
1147
|
+
scheduleReplicaSync(options, REPLICA_REFRESH_DELAY_MS);
|
|
1148
|
+
});
|
|
1149
|
+
scheduleReplicaSync(options, REPLICA_FIRST_DELAY_MS);
|
|
1150
|
+
}
|
|
1151
|
+
function scheduleReplicaSync(options, delayMs) {
|
|
1152
|
+
const state = replicaSyncs.get(replicaKeyFor(options.tenantId(), options.table));
|
|
1153
|
+
if (!state || state.timer) return;
|
|
1154
|
+
state.timer = setTimeout(() => {
|
|
1155
|
+
state.timer = null;
|
|
1156
|
+
if (state.running || Date.now() - state.lastSyncAt < REPLICA_MIN_INTERVAL_MS) return;
|
|
1157
|
+
state.running = true;
|
|
1158
|
+
void state.run().then(() => {
|
|
1159
|
+
state.lastSyncAt = Date.now();
|
|
1160
|
+
}).catch(() => {
|
|
1161
|
+
}).finally(() => {
|
|
1162
|
+
state.running = false;
|
|
1163
|
+
});
|
|
1164
|
+
}, delayMs);
|
|
1165
|
+
state.timer.unref?.();
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/fields/store.ts
|
|
1169
|
+
var FIELDS_TABLE = "plg_entity_fields";
|
|
1170
|
+
var CustomFieldsError = class extends Error {
|
|
1171
|
+
constructor(reference, issues) {
|
|
1172
|
+
super(`${reference} custom fields: ${issues.map((i) => i.message).join("; ")}`);
|
|
1173
|
+
this.name = "CustomFieldsError";
|
|
1174
|
+
this.issues = issues;
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
function readOptions(raw) {
|
|
1178
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1179
|
+
const options = raw.map((entry) => {
|
|
1180
|
+
if (typeof entry === "string") return { value: entry };
|
|
1181
|
+
const record = entry;
|
|
1182
|
+
return typeof record?.value === "string" ? { value: record.value, label: typeof record.label === "string" ? record.label : void 0 } : null;
|
|
1183
|
+
}).filter((option) => option !== null);
|
|
1184
|
+
return options.length ? options : void 0;
|
|
1185
|
+
}
|
|
1186
|
+
function rowToField(row) {
|
|
1187
|
+
const field = { type: row.type };
|
|
1188
|
+
if (row.label) field.label = row.label;
|
|
1189
|
+
if (row.help) field.help = row.help;
|
|
1190
|
+
if (row.required) field.required = true;
|
|
1191
|
+
if (row.default_value !== null && row.default_value !== void 0) field.default = row.default_value;
|
|
1192
|
+
const options = readOptions(row.options);
|
|
1193
|
+
if (options) field.options = options;
|
|
1194
|
+
if (row.validation && Object.keys(row.validation).length) {
|
|
1195
|
+
field.validation = row.validation;
|
|
1196
|
+
}
|
|
1197
|
+
return field;
|
|
1198
|
+
}
|
|
1199
|
+
var EMPTY = { reference: "", fields: {}, order: [] };
|
|
1200
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
1201
|
+
function clearCustomFieldsCache() {
|
|
1202
|
+
cache2.clear();
|
|
1203
|
+
}
|
|
1204
|
+
async function load(reference, tenantId) {
|
|
1205
|
+
const declared = declaredCustomFields(reference);
|
|
1206
|
+
const supabase = getSupabaseClientOptional();
|
|
1207
|
+
if (!supabase) return { reference, fields: { ...declared }, order: Object.keys(declared) };
|
|
1208
|
+
const { data, error } = await supabase.from(FIELDS_TABLE).select("reference,key,type,label,help,required,default_value,validation,options,position,origin").eq("reference", reference).is("archived_at", null).order("position", { ascending: true });
|
|
1209
|
+
if (error) return { reference, fields: { ...declared }, order: Object.keys(declared) };
|
|
1210
|
+
const fields = {};
|
|
1211
|
+
const order = [];
|
|
1212
|
+
for (const row of data ?? []) {
|
|
1213
|
+
if (fields[row.key]) continue;
|
|
1214
|
+
fields[row.key] = rowToField(row);
|
|
1215
|
+
order.push(row.key);
|
|
1216
|
+
}
|
|
1217
|
+
for (const [key, field] of Object.entries(declared)) {
|
|
1218
|
+
if (!fields[key]) order.push(key);
|
|
1219
|
+
fields[key] = field;
|
|
1220
|
+
}
|
|
1221
|
+
return { reference, fields, order };
|
|
1222
|
+
}
|
|
1223
|
+
function resolveCustomFields(reference, tenantId) {
|
|
1224
|
+
const tenant = tenantId ?? getActiveTenantId();
|
|
1225
|
+
if (!tenant) return Promise.resolve({ ...EMPTY, reference, fields: { ...declaredCustomFields(reference) } });
|
|
1226
|
+
const cacheKey2 = `${tenant}:${reference}`;
|
|
1227
|
+
let pending2 = cache2.get(cacheKey2);
|
|
1228
|
+
if (!pending2) {
|
|
1229
|
+
pending2 = load(reference).catch((err) => {
|
|
1230
|
+
cache2.delete(cacheKey2);
|
|
1231
|
+
throw err;
|
|
1232
|
+
});
|
|
1233
|
+
cache2.set(cacheKey2, pending2);
|
|
1234
|
+
}
|
|
1235
|
+
return pending2;
|
|
1236
|
+
}
|
|
1237
|
+
async function validateCustomFields(reference, values, tenantId) {
|
|
1238
|
+
const { fields } = await resolveCustomFields(reference, tenantId);
|
|
1239
|
+
const issues = validateEntityRecordData({ fields }, values);
|
|
1240
|
+
if (issues.length) throw new CustomFieldsError(reference, issues);
|
|
1241
|
+
}
|
|
1242
|
+
function mergeCustomFields(current, patch) {
|
|
1243
|
+
const merged = { ...current ?? {} };
|
|
1244
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1245
|
+
if (value === null) delete merged[key];
|
|
1246
|
+
else merged[key] = value;
|
|
1247
|
+
}
|
|
1248
|
+
return merged;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/data/custom-fields.ts
|
|
1252
|
+
var KEY = "customFields";
|
|
1253
|
+
function patchOf(data) {
|
|
1254
|
+
const value = data?.[KEY];
|
|
1255
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1256
|
+
}
|
|
1257
|
+
function withCustomFields(provider, reference) {
|
|
1258
|
+
return {
|
|
1259
|
+
...provider,
|
|
1260
|
+
async create(data) {
|
|
1261
|
+
const patch = patchOf(data);
|
|
1262
|
+
if (!patch) return provider.create(data);
|
|
1263
|
+
await validateCustomFields(reference, patch);
|
|
1264
|
+
return provider.create(data);
|
|
1265
|
+
},
|
|
1266
|
+
async update(id, data) {
|
|
1267
|
+
const patch = patchOf(data);
|
|
1268
|
+
if (!patch) return provider.update(id, data);
|
|
1269
|
+
await validateCustomFields(reference, patch);
|
|
1270
|
+
const { data: rows } = await provider.list({
|
|
1271
|
+
filters: { id },
|
|
1272
|
+
pageSize: 1,
|
|
1273
|
+
countMode: "none"
|
|
1274
|
+
});
|
|
1275
|
+
const current = rows[0]?.[KEY];
|
|
1276
|
+
const merged = mergeCustomFields(current, patch);
|
|
1277
|
+
return provider.update(id, { ...data, [KEY]: merged });
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/data/resolve.ts
|
|
1283
|
+
function resolveDataProvider(entityDef, mockData, options = {}) {
|
|
1284
|
+
const backend = options.backend;
|
|
1285
|
+
if (backend?.provider === "mock") {
|
|
1286
|
+
return createMockProvider(entityDef, mockData);
|
|
1287
|
+
}
|
|
1288
|
+
if (backend?.provider === "fayz-api") {
|
|
1289
|
+
const table = entityDef.data?.table ?? entityDef.name;
|
|
1290
|
+
return createFayzApiProvider(table, {
|
|
1291
|
+
...options.fayzApi,
|
|
1292
|
+
baseUrl: options.fayzApi?.baseUrl ?? backend.url,
|
|
1293
|
+
projectId: options.fayzApi?.projectId ?? backend.projectRef,
|
|
1294
|
+
entityKey: entityDef.name ?? table,
|
|
1295
|
+
table,
|
|
1296
|
+
schema: options.fayzApi?.schema ?? entityDef.data?.schema,
|
|
1297
|
+
idColumn: options.fayzApi?.idColumn ?? entityDef.data?.columnMap?.id,
|
|
1298
|
+
searchColumns: options.fayzApi?.searchColumns ?? entityDef.data?.searchColumns ?? entityDef.fields.filter((field) => field.searchable).map((field) => field.key),
|
|
1299
|
+
tenantIdColumn: entityDef.data?.tenantScoped === false ? false : options.fayzApi?.tenantIdColumn ?? entityDef.data?.tenantIdColumn,
|
|
1300
|
+
tenantId: options.fayzApi?.tenantId ?? (() => getActiveTenantId())
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
if (backend?.provider === "custom") {
|
|
1304
|
+
const adapterId = backend.adapterId;
|
|
1305
|
+
const factory = adapterId ? options.customProviders?.[adapterId] : void 0;
|
|
1306
|
+
if (!adapterId || !factory) {
|
|
1307
|
+
throw new Error(`[@fayz-ai/core] Custom data provider "${adapterId ?? "unknown"}" is not registered.`);
|
|
1308
|
+
}
|
|
1309
|
+
return factory(entityDef, mockData);
|
|
1310
|
+
}
|
|
1311
|
+
const client = getSupabaseClientOptional();
|
|
1312
|
+
if (client && entityDef.data?.table) {
|
|
1313
|
+
const tenantId = () => getActiveTenantId();
|
|
1314
|
+
const cacheOptions = {
|
|
1315
|
+
table: entityDef.data.archetypeKind ? `${entityDef.data.table}#${entityDef.data.archetypeKind}` : entityDef.data.table,
|
|
1316
|
+
tenantId,
|
|
1317
|
+
ttl: entityDef.data.cacheTTL
|
|
1318
|
+
};
|
|
1319
|
+
const offlinePolicy = entityDef.data.offline ?? "online";
|
|
1320
|
+
const cached = (p) => {
|
|
1321
|
+
const withL1 = options.cache === false ? p : withCache(p, cacheOptions);
|
|
1322
|
+
return withOffline(withL1, { ...cacheOptions, policy: offlinePolicy, entityDef });
|
|
1323
|
+
};
|
|
1324
|
+
if (entityDef.data.archetype && entityDef.data.archetypeKind && !entityDef.data.schema) {
|
|
1325
|
+
return cached(withCustomFields(createArchetypeProvider({
|
|
1326
|
+
archetype: entityDef.data.archetype,
|
|
1327
|
+
archetypeKind: entityDef.data.archetypeKind,
|
|
1328
|
+
projectTable: entityDef.data.table,
|
|
1329
|
+
tenantId,
|
|
1330
|
+
searchColumns: entityDef.data.searchColumns ?? entityDef.fields.filter((field) => field.searchable).map((field) => field.key)
|
|
1331
|
+
}), entityDef.data.archetype));
|
|
1332
|
+
}
|
|
1333
|
+
return cached(createSupabaseProvider(entityDef.data.table, {
|
|
1334
|
+
schema: entityDef.data.schema,
|
|
1335
|
+
tenantId: entityDef.data.tenantScoped === false ? void 0 : tenantId,
|
|
1336
|
+
tenantIdColumn: entityDef.data.tenantIdColumn,
|
|
1337
|
+
searchColumns: entityDef.data.searchColumns ?? entityDef.fields.filter((field) => field.searchable).map((field) => field.key),
|
|
1338
|
+
selectColumns: entityDef.data.selectColumns,
|
|
1339
|
+
columnMap: entityDef.data.columnMap,
|
|
1340
|
+
filters: entityDef.data.filters,
|
|
1341
|
+
defaults: entityDef.data.defaults
|
|
1342
|
+
}));
|
|
1343
|
+
}
|
|
1344
|
+
if (entityDef.data?.table) {
|
|
1345
|
+
console.warn(
|
|
1346
|
+
`[fayz/core] resolveDataProvider: falling back to mock for "${entityDef.name}" (table: ${entityDef.data.table}). Supabase client ${client ? "available" : "NOT initialized"}.`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
return createMockProvider(entityDef, mockData);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/data/query.ts
|
|
1353
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
1354
|
+
var DEFAULT_RETRY_DELAY_MS = 150;
|
|
1355
|
+
var telemetryHook = null;
|
|
1356
|
+
function registerDataTelemetry(fn) {
|
|
1357
|
+
const previous = telemetryHook;
|
|
1358
|
+
telemetryHook = fn;
|
|
1359
|
+
return () => {
|
|
1360
|
+
telemetryHook = previous;
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function report(event) {
|
|
1364
|
+
if (!telemetryHook) return;
|
|
1365
|
+
try {
|
|
1366
|
+
telemetryHook(event);
|
|
1367
|
+
} catch {
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function getClient(schema) {
|
|
1371
|
+
const client = getSupabaseClientOptional();
|
|
1372
|
+
if (!client) throw new DataError("invalid", "supabase client not initialized");
|
|
1373
|
+
if (!schema || schema === "public") return client;
|
|
1374
|
+
if (typeof client.schema !== "function") {
|
|
1375
|
+
throw new DataError("invalid", `supabase client cannot reach schema "${schema}"`);
|
|
1376
|
+
}
|
|
1377
|
+
return client.schema(schema);
|
|
1378
|
+
}
|
|
1379
|
+
function resolveTenant(options, target) {
|
|
1380
|
+
if (options.tenant === "none") return null;
|
|
1381
|
+
const id = getActiveTenantId();
|
|
1382
|
+
if (!id) {
|
|
1383
|
+
throw new DataError(
|
|
1384
|
+
"invalid",
|
|
1385
|
+
`no active tenant for "${target}" \u2014 set one with setActiveTenantId, or pass tenant: 'none'`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
return { column: options.tenantColumn ?? "tenant_id", arg: options.tenantArg, id };
|
|
1389
|
+
}
|
|
1390
|
+
function targetOf(from, rpc) {
|
|
1391
|
+
return from ?? `rpc:${rpc?.name ?? "?"}`;
|
|
1392
|
+
}
|
|
1393
|
+
function cachePrefix(tenantId, table) {
|
|
1394
|
+
return `${tenantId ?? "_"}:${table}`;
|
|
1395
|
+
}
|
|
1396
|
+
function applyFilters(builder, filters) {
|
|
1397
|
+
if (!filters) return builder;
|
|
1398
|
+
let q = builder;
|
|
1399
|
+
for (const [column, value] of Object.entries(filters)) {
|
|
1400
|
+
if (value === void 0) continue;
|
|
1401
|
+
q = value === null ? q.is(column, null) : q.eq(column, value);
|
|
1402
|
+
}
|
|
1403
|
+
return q;
|
|
1404
|
+
}
|
|
1405
|
+
function withAbort(builder, signal) {
|
|
1406
|
+
return typeof builder.abortSignal === "function" ? builder.abortSignal(signal) : builder;
|
|
1407
|
+
}
|
|
1408
|
+
function delay(ms) {
|
|
1409
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1410
|
+
}
|
|
1411
|
+
async function runWithTimeout(build, timeoutMs) {
|
|
1412
|
+
const controller = new AbortController();
|
|
1413
|
+
let timer;
|
|
1414
|
+
const deadline = new Promise((_, reject) => {
|
|
1415
|
+
timer = setTimeout(() => {
|
|
1416
|
+
controller.abort();
|
|
1417
|
+
reject(new DataError("offline", `request timed out after ${timeoutMs}ms`));
|
|
1418
|
+
}, timeoutMs);
|
|
1419
|
+
});
|
|
1420
|
+
try {
|
|
1421
|
+
const pending2 = Promise.resolve(build(controller.signal));
|
|
1422
|
+
return await Promise.race([pending2, deadline]);
|
|
1423
|
+
} finally {
|
|
1424
|
+
if (timer) clearTimeout(timer);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function unwrap(response) {
|
|
1428
|
+
if (response?.error) throw toDataError(response.error);
|
|
1429
|
+
return response?.data;
|
|
1430
|
+
}
|
|
1431
|
+
async function dataQuery(options) {
|
|
1432
|
+
const started = Date.now();
|
|
1433
|
+
const target = targetOf(options.from, options.rpc);
|
|
1434
|
+
try {
|
|
1435
|
+
if (!options.from === !options.rpc) {
|
|
1436
|
+
throw new DataError("invalid", "dataQuery needs exactly one of `from` or `rpc`");
|
|
1437
|
+
}
|
|
1438
|
+
const tenant = resolveTenant(options, target);
|
|
1439
|
+
const cacheKey2 = options.key ?? `${cachePrefix(tenant?.id, target)}:${stableKey("q", {
|
|
1440
|
+
select: options.select,
|
|
1441
|
+
filters: options.filters,
|
|
1442
|
+
rpc: options.rpc?.args,
|
|
1443
|
+
order: options.order,
|
|
1444
|
+
limit: options.limit,
|
|
1445
|
+
page: options.page,
|
|
1446
|
+
single: options.single,
|
|
1447
|
+
schema: options.schema
|
|
1448
|
+
})}`;
|
|
1449
|
+
if (options.policy === "cache") {
|
|
1450
|
+
const hit = globalCache.get(cacheKey2);
|
|
1451
|
+
if (hit !== void 0) {
|
|
1452
|
+
report({ op: "query", target, durationMs: Date.now() - started, cached: true });
|
|
1453
|
+
return hit;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const attempts = 2;
|
|
1457
|
+
let lastError;
|
|
1458
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
1459
|
+
try {
|
|
1460
|
+
const rows = await executeQuery(options, tenant);
|
|
1461
|
+
if (options.policy === "cache") globalCache.set(cacheKey2, rows, options.ttl);
|
|
1462
|
+
report({ op: "query", target, durationMs: Date.now() - started });
|
|
1463
|
+
return rows;
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
lastError = toDataError(err);
|
|
1466
|
+
if (!lastError.retryable || attempt === attempts) throw lastError;
|
|
1467
|
+
await delay(options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
throw lastError ?? new DataError("server", `query on "${target}" produced no result`);
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
const error = toDataError(err);
|
|
1473
|
+
report({ op: "query", target, durationMs: Date.now() - started, error });
|
|
1474
|
+
throw error;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
async function executeQuery(options, tenant) {
|
|
1478
|
+
const client = getClient(options.schema);
|
|
1479
|
+
const response = await runWithTimeout((signal) => {
|
|
1480
|
+
let q;
|
|
1481
|
+
if (options.rpc) {
|
|
1482
|
+
const args = { ...options.rpc.args };
|
|
1483
|
+
if (tenant?.arg && args[tenant.arg] === void 0) args[tenant.arg] = tenant.id;
|
|
1484
|
+
q = client.rpc(options.rpc.name, args);
|
|
1485
|
+
if (options.select) q = q.select(options.select);
|
|
1486
|
+
} else {
|
|
1487
|
+
q = client.from(options.from).select(options.select ?? "*");
|
|
1488
|
+
if (tenant) q = q.eq(tenant.column, tenant.id);
|
|
1489
|
+
}
|
|
1490
|
+
q = applyFilters(q, options.filters);
|
|
1491
|
+
if (options.apply) q = options.apply(q);
|
|
1492
|
+
if (options.order) q = q.order(options.order.column, { ascending: options.order.ascending !== false });
|
|
1493
|
+
if (options.limit !== void 0) {
|
|
1494
|
+
const page = Math.max(1, options.page ?? 1);
|
|
1495
|
+
const from = (page - 1) * options.limit;
|
|
1496
|
+
q = page > 1 ? q.range(from, from + options.limit - 1) : q.limit(options.limit);
|
|
1497
|
+
}
|
|
1498
|
+
if (options.single) q = q.single();
|
|
1499
|
+
return withAbort(q, signal);
|
|
1500
|
+
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1501
|
+
const data = unwrap(response);
|
|
1502
|
+
if (options.single) return data;
|
|
1503
|
+
return data ?? [];
|
|
1504
|
+
}
|
|
1505
|
+
async function dataCall(options) {
|
|
1506
|
+
const started = Date.now();
|
|
1507
|
+
const target = targetOf(options.from, options.rpc);
|
|
1508
|
+
try {
|
|
1509
|
+
if (!options.from === !options.rpc) {
|
|
1510
|
+
throw new DataError("invalid", "dataCall needs exactly one of `from` or `rpc`");
|
|
1511
|
+
}
|
|
1512
|
+
if (options.from && !options.action) {
|
|
1513
|
+
throw new DataError("invalid", `dataCall on "${options.from}" needs an action`);
|
|
1514
|
+
}
|
|
1515
|
+
const tenant = resolveTenant(options, target);
|
|
1516
|
+
const client = getClient(options.schema);
|
|
1517
|
+
const isDelete = options.action === "delete";
|
|
1518
|
+
const returning = options.returning ?? !isDelete;
|
|
1519
|
+
const single = options.single ?? (returning && !Array.isArray(options.values));
|
|
1520
|
+
const response = await runWithTimeout((signal) => {
|
|
1521
|
+
let q;
|
|
1522
|
+
if (options.rpc) {
|
|
1523
|
+
const args = { ...options.rpc.args };
|
|
1524
|
+
if (tenant?.arg && args[tenant.arg] === void 0) args[tenant.arg] = tenant.id;
|
|
1525
|
+
q = client.rpc(options.rpc.name, args);
|
|
1526
|
+
} else {
|
|
1527
|
+
const table = client.from(options.from);
|
|
1528
|
+
const values = withTenantValues(options.values, tenant, options.action);
|
|
1529
|
+
const match = { ...options.match };
|
|
1530
|
+
if (tenant && (options.action === "update" || options.action === "delete")) {
|
|
1531
|
+
match[tenant.column] = tenant.id;
|
|
1532
|
+
}
|
|
1533
|
+
q = options.action === "insert" ? table.insert(values) : options.action === "upsert" ? table.upsert(values) : options.action === "update" ? table.update(values) : table.delete();
|
|
1534
|
+
q = applyFilters(q, match);
|
|
1535
|
+
if (returning) q = q.select(options.select ?? "*");
|
|
1536
|
+
if (single) q = q.single();
|
|
1537
|
+
}
|
|
1538
|
+
return withAbort(q, signal);
|
|
1539
|
+
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1540
|
+
const data = unwrap(response);
|
|
1541
|
+
if (options.from) {
|
|
1542
|
+
globalCache.invalidate(cachePrefix(tenant?.id ?? getActiveTenantId(), options.from));
|
|
1543
|
+
emitDataChanged({ table: options.from, op: changeOp(options.action), source: "ui" });
|
|
1544
|
+
}
|
|
1545
|
+
for (const table of options.invalidates ?? []) {
|
|
1546
|
+
globalCache.invalidate(cachePrefix(tenant?.id ?? getActiveTenantId(), table));
|
|
1547
|
+
emitDataChanged({ table, op: "unknown", source: "ui" });
|
|
1548
|
+
}
|
|
1549
|
+
report({ op: "call", target, durationMs: Date.now() - started });
|
|
1550
|
+
return data;
|
|
1551
|
+
} catch (err) {
|
|
1552
|
+
const error = toDataError(err);
|
|
1553
|
+
report({ op: "call", target, durationMs: Date.now() - started, error });
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
function changeOp(action) {
|
|
1558
|
+
if (action === "insert") return "create";
|
|
1559
|
+
if (action === "delete") return "delete";
|
|
1560
|
+
return "update";
|
|
1561
|
+
}
|
|
1562
|
+
function withTenantValues(values, tenant, action) {
|
|
1563
|
+
if (values === void 0) return void 0;
|
|
1564
|
+
if (!tenant || action === "update") return values;
|
|
1565
|
+
const stamp = (row) => row[tenant.column] === void 0 ? { ...row, [tenant.column]: tenant.id } : row;
|
|
1566
|
+
return Array.isArray(values) ? values.map(stamp) : stamp(values);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/data/bulk.ts
|
|
1570
|
+
async function bulkCreate(provider, rows, options) {
|
|
1571
|
+
if (!rows.length) return { created: 0, failed: [] };
|
|
1572
|
+
if (provider.createMany) return provider.createMany(rows, options);
|
|
1573
|
+
let created = 0;
|
|
1574
|
+
const failed = [];
|
|
1575
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1576
|
+
try {
|
|
1577
|
+
await provider.create(rows[i]);
|
|
1578
|
+
created += 1;
|
|
1579
|
+
} catch (error) {
|
|
1580
|
+
failed.push({ index: i, message: error instanceof Error ? error.message : String(error) });
|
|
1581
|
+
}
|
|
1582
|
+
options?.onProgress?.(i + 1, rows.length);
|
|
1583
|
+
}
|
|
1584
|
+
return { created, failed };
|
|
1585
|
+
}
|
|
1586
|
+
async function bulkRemove(provider, ids, options) {
|
|
1587
|
+
if (!ids.length) return { removed: 0, failed: [] };
|
|
1588
|
+
const failed = [];
|
|
1589
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
1590
|
+
if (provider.removeMany) {
|
|
1591
|
+
const chunkSize = options?.chunkSize ?? 100;
|
|
1592
|
+
let removed2 = 0;
|
|
1593
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
1594
|
+
const chunk = ids.slice(i, i + chunkSize);
|
|
1595
|
+
try {
|
|
1596
|
+
await provider.removeMany(chunk);
|
|
1597
|
+
removed2 += chunk.length;
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
for (const id of chunk) failed.push({ id, message: message(error) });
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return { removed: removed2, failed };
|
|
1603
|
+
}
|
|
1604
|
+
let removed = 0;
|
|
1605
|
+
for (const id of ids) {
|
|
1606
|
+
try {
|
|
1607
|
+
await provider.remove(id);
|
|
1608
|
+
removed += 1;
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
failed.push({ id, message: message(error) });
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return { removed, failed };
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
export { CustomFieldsError, DATA_CHANGED_EVENT, DataError, FIELDS_TABLE, attachPersistentCache, bulkCreate, bulkRemove, cacheKeyFor, cachePrefixFor, clearCustomFieldsCache, countByTenant, createArchetypeProvider, createEventBus, createFayzApiProvider, createMockProvider, dataCall, dataQuery, emitDataChanged, eventBus, getPersistentCache, invalidateCount, localList, matchesDataChange, mergeCustomFields, registerDataTelemetry, replicaKeyFor, resolveCustomFields, resolveDataProvider, syncReplica, toDataError, useDataChanged, validateCustomFields, withCache, withOffline };
|
|
1617
|
+
//# sourceMappingURL=chunk-S5FOFKR4.js.map
|
|
1618
|
+
//# sourceMappingURL=chunk-S5FOFKR4.js.map
|