@rebasepro/common 0.9.1-canary.fd3754b → 0.10.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/collections/default-collections.d.ts +4 -0
- package/dist/data/buildRebaseData.d.ts +15 -2
- package/dist/index.es.js +676 -46
- package/dist/index.es.js.map +1 -1
- package/dist/util/auth-default-policies.d.ts +22 -0
- package/dist/util/identity.d.ts +83 -0
- package/dist/util/index.d.ts +3 -0
- package/dist/util/junction-policies.d.ts +108 -0
- package/dist/util/policy/evaluatePolicy.d.ts +8 -1
- package/dist/util/policy/index.d.ts +1 -0
- package/dist/util/policy/sqlToPolicy.d.ts +24 -14
- package/package.json +7 -8
- package/src/collections/default-collections.ts +2 -0
- package/src/data/buildRebaseData.ts +119 -24
- package/src/util/auth-default-policies.ts +152 -0
- package/src/util/identity.ts +166 -0
- package/src/util/index.ts +3 -0
- package/src/util/junction-policies.ts +353 -0
- package/src/util/policy/evaluatePolicy.ts +20 -4
- package/src/util/policy/index.ts +1 -0
- package/src/util/policy/policyToPostgres.ts +9 -4
- package/src/util/policy/sqlToPolicy.ts +196 -16
- package/dist/index.umd.js +0 -3328
- package/dist/index.umd.js.map +0 -1
package/dist/index.umd.js
DELETED
|
@@ -1,3328 +0,0 @@
|
|
|
1
|
-
(function(global, factory) {
|
|
2
|
-
typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("@rebasepro/types"), require("@rebasepro/utils"), require("json-logic-js"), require("fast-equals")) : typeof define === "function" && define.amd ? define([
|
|
3
|
-
"exports",
|
|
4
|
-
"@rebasepro/types",
|
|
5
|
-
"@rebasepro/utils",
|
|
6
|
-
"json-logic-js",
|
|
7
|
-
"fast-equals"
|
|
8
|
-
], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["Rebase Util"] = {}, global._rebasepro_types, global._rebasepro_utils, global.jsonLogic, global.fastEquals));
|
|
9
|
-
})(this, function(exports, _rebasepro_types, _rebasepro_utils, json_logic_js, fast_equals) {
|
|
10
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
11
|
-
//#region \0rolldown/runtime.js
|
|
12
|
-
var __create = Object.create;
|
|
13
|
-
var __defProp = Object.defineProperty;
|
|
14
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
15
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
16
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
17
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
18
|
-
var __copyProps = (to, from, except, desc) => {
|
|
19
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
20
|
-
key = keys[i];
|
|
21
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
22
|
-
get: ((k) => from[k]).bind(null, key),
|
|
23
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
return to;
|
|
27
|
-
};
|
|
28
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
29
|
-
value: mod,
|
|
30
|
-
enumerable: true
|
|
31
|
-
}) : target, mod));
|
|
32
|
-
//#endregion
|
|
33
|
-
json_logic_js = __toESM(json_logic_js, 1);
|
|
34
|
-
//#region src/util/common.ts
|
|
35
|
-
var DEFAULT_ONE_OF_TYPE = "type";
|
|
36
|
-
var DEFAULT_ONE_OF_VALUE = "value";
|
|
37
|
-
//#endregion
|
|
38
|
-
//#region src/util/entities.ts
|
|
39
|
-
function isReadOnly(property) {
|
|
40
|
-
if (property.ui?.readOnly) return true;
|
|
41
|
-
if (property.type === "date") {
|
|
42
|
-
if (property.autoValue) return true;
|
|
43
|
-
}
|
|
44
|
-
if (property.type === "reference") return !property.path && !("Field" in (property.ui || {}) && property.ui?.Field);
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
function isHidden(property) {
|
|
48
|
-
return typeof property.ui?.disabled === "object" && Boolean(property.ui?.disabled.hidden);
|
|
49
|
-
}
|
|
50
|
-
function isPropertyBuilder(property) {
|
|
51
|
-
return typeof property?.dynamicProps === "function";
|
|
52
|
-
}
|
|
53
|
-
function getDefaultValuesFor(properties) {
|
|
54
|
-
if (!properties) return {};
|
|
55
|
-
return Object.entries(properties).map(([key, property]) => {
|
|
56
|
-
if (!property) return {};
|
|
57
|
-
const value = getDefaultValueFor(property);
|
|
58
|
-
return value === void 0 ? {} : { [key]: value };
|
|
59
|
-
}).reduce((a, b) => ({
|
|
60
|
-
...a,
|
|
61
|
-
...b
|
|
62
|
-
}), {});
|
|
63
|
-
}
|
|
64
|
-
function getDefaultValueFor(property) {
|
|
65
|
-
if (!property) return void 0;
|
|
66
|
-
if (isPropertyBuilder(property)) return void 0;
|
|
67
|
-
if (property.defaultValue || property.defaultValue === null) return property.defaultValue;
|
|
68
|
-
else if (property.type === "map" && property.properties) {
|
|
69
|
-
const defaultValuesFor = getDefaultValuesFor(property.properties);
|
|
70
|
-
if (Object.keys(defaultValuesFor).length === 0) return void 0;
|
|
71
|
-
return defaultValuesFor;
|
|
72
|
-
} else return getDefaultValueFortype(property.type);
|
|
73
|
-
}
|
|
74
|
-
function getDefaultValueFortype(type) {
|
|
75
|
-
if (type === "string") return null;
|
|
76
|
-
else if (type === "number") return null;
|
|
77
|
-
else if (type === "boolean") return false;
|
|
78
|
-
else if (type === "date") return null;
|
|
79
|
-
else if (type === "array") return [];
|
|
80
|
-
else if (type === "map") return {};
|
|
81
|
-
else if (type === "vector") return null;
|
|
82
|
-
else if (type === "binary") return null;
|
|
83
|
-
else return null;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Update the automatic values in a entity before save
|
|
87
|
-
* @group Driver
|
|
88
|
-
*/
|
|
89
|
-
function updateDateAutoValues({ inputValues, properties, status, timestampNowValue }) {
|
|
90
|
-
return traverseValuesProperties(inputValues, properties, (inputValue, property) => {
|
|
91
|
-
if (property.type === "date") if (status === "existing" && property.autoValue === "on_update") return timestampNowValue;
|
|
92
|
-
else if ((status === "new" || status === "copy") && (property.autoValue === "on_update" || property.autoValue === "on_create")) return timestampNowValue;
|
|
93
|
-
else return inputValue;
|
|
94
|
-
else return inputValue;
|
|
95
|
-
}) ?? {};
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Add missing required fields, expected in the collection, to the values of a entity
|
|
99
|
-
* @param values
|
|
100
|
-
* @param properties
|
|
101
|
-
* @group Driver
|
|
102
|
-
*/
|
|
103
|
-
function sanitizeData(values, properties) {
|
|
104
|
-
const result = values;
|
|
105
|
-
Object.entries(properties).forEach(([key, property]) => {
|
|
106
|
-
if (values && values[key] !== void 0) result[key] = values[key];
|
|
107
|
-
else if (property.validation?.required) result[key] = null;
|
|
108
|
-
});
|
|
109
|
-
return result;
|
|
110
|
-
}
|
|
111
|
-
function getReferenceFrom(entity) {
|
|
112
|
-
if (typeof entity.id !== "string") throw new Error("Only string IDs are supported in references");
|
|
113
|
-
return new _rebasepro_types.EntityReference({
|
|
114
|
-
id: entity.id,
|
|
115
|
-
path: entity.path,
|
|
116
|
-
driver: entity.driver,
|
|
117
|
-
databaseId: entity.databaseId
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
function getRelationFrom(entity) {
|
|
121
|
-
return new _rebasepro_types.EntityRelation(entity.id, entity.path, entity);
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Normalize a value into a proper EntityRelation instance.
|
|
125
|
-
* Handles EntityRelation class instances, and plain objects
|
|
126
|
-
* with `__type === "relation"` or an `isEntityRelation()` method.
|
|
127
|
-
*
|
|
128
|
-
* When `propertyType` is `"relation"`, also accepts plain objects that
|
|
129
|
-
* have `id` and `path` fields — these are relation-shaped objects from
|
|
130
|
-
* edge cases in the data pipeline (REST fallback, stale cache, custom data source).
|
|
131
|
-
*
|
|
132
|
-
* Returns null if the value cannot be coerced.
|
|
133
|
-
*/
|
|
134
|
-
function normalizeToEntityRelation(value, propertyType) {
|
|
135
|
-
if (value instanceof _rebasepro_types.EntityRelation) return value;
|
|
136
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
137
|
-
const obj = value;
|
|
138
|
-
if (!(obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference() || propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string")) return null;
|
|
139
|
-
return new _rebasepro_types.EntityRelation(obj.id, obj.path, obj.data);
|
|
140
|
-
}
|
|
141
|
-
function traverseValuesProperties(inputValues, properties, operation) {
|
|
142
|
-
const safeInputValues = inputValues ?? {};
|
|
143
|
-
const result = (0, _rebasepro_utils.mergeDeep)(safeInputValues, Object.entries(properties).map(([key, property]) => {
|
|
144
|
-
const updatedValue = traverseValueProperty(safeInputValues && safeInputValues[key], property, operation);
|
|
145
|
-
if (updatedValue === null) return null;
|
|
146
|
-
if (updatedValue === void 0) return void 0;
|
|
147
|
-
return { [key]: updatedValue };
|
|
148
|
-
}).reduce((a, b) => ({
|
|
149
|
-
...a,
|
|
150
|
-
...b
|
|
151
|
-
}), {}));
|
|
152
|
-
if (!result || Object.keys(result).length === 0) return void 0;
|
|
153
|
-
return result;
|
|
154
|
-
}
|
|
155
|
-
function traverseValueProperty(inputValue, property, operation) {
|
|
156
|
-
let value;
|
|
157
|
-
if (property.type === "map" && property.properties) value = traverseValuesProperties(inputValue, property.properties, operation);
|
|
158
|
-
else if (property.type === "array") {
|
|
159
|
-
const of = property.of;
|
|
160
|
-
if (of && Array.isArray(inputValue) && !Array.isArray(of)) value = inputValue.map((e) => traverseValueProperty(e, of, operation));
|
|
161
|
-
else if (of && Array.isArray(inputValue) && Array.isArray(of)) value = inputValue.map((e, i) => {
|
|
162
|
-
if (i < of.length) return traverseValueProperty(e, of[i], operation);
|
|
163
|
-
return null;
|
|
164
|
-
}).filter(Boolean);
|
|
165
|
-
else if (property.oneOf && Array.isArray(inputValue)) {
|
|
166
|
-
const typeField = property.oneOf?.typeField ?? "type";
|
|
167
|
-
const valueField = property.oneOf?.valueField ?? "value";
|
|
168
|
-
value = inputValue.map((e) => {
|
|
169
|
-
if (e === null) return null;
|
|
170
|
-
if (typeof e !== "object") return e;
|
|
171
|
-
const rec = e;
|
|
172
|
-
const type = rec[typeField];
|
|
173
|
-
const childProperty = property.oneOf?.properties[type];
|
|
174
|
-
if (!type || !childProperty) return e;
|
|
175
|
-
return {
|
|
176
|
-
[typeField]: type,
|
|
177
|
-
[valueField]: traverseValueProperty(rec[valueField], childProperty, operation)
|
|
178
|
-
};
|
|
179
|
-
});
|
|
180
|
-
} else value = inputValue;
|
|
181
|
-
} else value = operation(inputValue, property);
|
|
182
|
-
return value;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Create a lightweight relation stub for CMS views.
|
|
186
|
-
* Replaces inline `{ id, path, __type: "relation" }` object literals.
|
|
187
|
-
*/
|
|
188
|
-
function createRelationRef(id, path) {
|
|
189
|
-
return {
|
|
190
|
-
id,
|
|
191
|
-
path,
|
|
192
|
-
__type: "relation"
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Create a hydrated relation reference that includes the full entity data.
|
|
197
|
-
* Used when entity data has been pre-fetched (e.g., via batch loading or JOINs).
|
|
198
|
-
*/
|
|
199
|
-
function createRelationRefWithData(id, path, data) {
|
|
200
|
-
return {
|
|
201
|
-
id,
|
|
202
|
-
path,
|
|
203
|
-
__type: "relation",
|
|
204
|
-
data
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
//#endregion
|
|
208
|
-
//#region src/util/collections.ts
|
|
209
|
-
function sortProperties(properties, propertiesOrder) {
|
|
210
|
-
try {
|
|
211
|
-
const propertiesKeys = Object.keys(properties);
|
|
212
|
-
if (!propertiesOrder || propertiesOrder.length === 0) return propertiesKeys.map((key) => {
|
|
213
|
-
const property = properties[key];
|
|
214
|
-
if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) return { [key]: {
|
|
215
|
-
...property,
|
|
216
|
-
properties: sortProperties(property.properties, property.propertiesOrder)
|
|
217
|
-
} };
|
|
218
|
-
else return { [key]: property };
|
|
219
|
-
}).reduce((a, b) => ({
|
|
220
|
-
...a,
|
|
221
|
-
...b
|
|
222
|
-
}), {});
|
|
223
|
-
const validOrderKeys = propertiesOrder.filter((key) => {
|
|
224
|
-
return !key.includes(".") && properties[key];
|
|
225
|
-
});
|
|
226
|
-
const processedKeys = new Set(validOrderKeys);
|
|
227
|
-
const orderedResult = validOrderKeys.map((key) => {
|
|
228
|
-
const property = properties[key];
|
|
229
|
-
if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) return { [key]: {
|
|
230
|
-
...property,
|
|
231
|
-
properties: sortProperties(property.properties, property.propertiesOrder)
|
|
232
|
-
} };
|
|
233
|
-
else return { [key]: property };
|
|
234
|
-
}).reduce((a, b) => ({
|
|
235
|
-
...a,
|
|
236
|
-
...b
|
|
237
|
-
}), {});
|
|
238
|
-
const missingProperties = propertiesKeys.filter((key) => !processedKeys.has(key)).map((key) => {
|
|
239
|
-
const property = properties[key];
|
|
240
|
-
if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) return { [key]: {
|
|
241
|
-
...property,
|
|
242
|
-
properties: sortProperties(property.properties, property.propertiesOrder)
|
|
243
|
-
} };
|
|
244
|
-
else return { [key]: property };
|
|
245
|
-
}).reduce((a, b) => ({
|
|
246
|
-
...a,
|
|
247
|
-
...b
|
|
248
|
-
}), {});
|
|
249
|
-
return {
|
|
250
|
-
...orderedResult,
|
|
251
|
-
...missingProperties
|
|
252
|
-
};
|
|
253
|
-
} catch (e) {
|
|
254
|
-
console.error("Error sorting properties", e);
|
|
255
|
-
return properties;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function resolveDefaultSelectedView(defaultSelectedView, params) {
|
|
259
|
-
if (!defaultSelectedView) return;
|
|
260
|
-
else if (typeof defaultSelectedView === "string") return defaultSelectedView;
|
|
261
|
-
else return defaultSelectedView(params);
|
|
262
|
-
}
|
|
263
|
-
function getLocalChangesBackup(collection) {
|
|
264
|
-
if (!collection.localChangesBackup) return "manual_apply";
|
|
265
|
-
return collection.localChangesBackup;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Returns the primary keys for a entity collection by inspecting the properties
|
|
269
|
-
* and finding any properties with `isId`.
|
|
270
|
-
* Fallbacks to `["id"]` if no properties are marked as `isId: true`.
|
|
271
|
-
* @param collection
|
|
272
|
-
*/
|
|
273
|
-
function getPrimaryKeys(collection) {
|
|
274
|
-
const properties = collection.properties;
|
|
275
|
-
if (!properties) return ["id"];
|
|
276
|
-
const ids = Object.entries(properties).filter(([key, prop]) => typeof prop === "object" && prop !== null && "isId" in prop && Boolean(prop.isId)).map(([key]) => key);
|
|
277
|
-
if (ids.length > 0) return ids;
|
|
278
|
-
return ["id"];
|
|
279
|
-
}
|
|
280
|
-
//#endregion
|
|
281
|
-
//#region src/util/enums.ts
|
|
282
|
-
function enumToObjectEntries(enumValues) {
|
|
283
|
-
if (Array.isArray(enumValues)) return enumValues;
|
|
284
|
-
else return Object.entries(enumValues).map(([id, value]) => {
|
|
285
|
-
if (typeof value === "string") return {
|
|
286
|
-
id,
|
|
287
|
-
label: value
|
|
288
|
-
};
|
|
289
|
-
else return {
|
|
290
|
-
...value,
|
|
291
|
-
id
|
|
292
|
-
};
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
function getLabelOrConfigFrom(enumValues, key) {
|
|
296
|
-
if (key === null || key === void 0) return void 0;
|
|
297
|
-
return enumValues.find((entry) => String(entry.id) === String(key));
|
|
298
|
-
}
|
|
299
|
-
//#endregion
|
|
300
|
-
//#region src/util/paths.ts
|
|
301
|
-
var COLLECTION_PATH_SEPARATOR = "::";
|
|
302
|
-
/**
|
|
303
|
-
* Remove the entity ids from a given path
|
|
304
|
-
* `products/B44RG6APH/locales` => `products::locales`
|
|
305
|
-
* @param path
|
|
306
|
-
*/
|
|
307
|
-
function stripCollectionPath(path) {
|
|
308
|
-
return segmentsToStrippedPath(fullPathToCollectionSegments(path));
|
|
309
|
-
}
|
|
310
|
-
function segmentsToStrippedPath(paths) {
|
|
311
|
-
if (paths.length === 1) return paths[0];
|
|
312
|
-
return paths.reduce((a, b) => `${a}::${b}`);
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Extract the collection path routes
|
|
316
|
-
* `products/B44RG6APH/locales` => [`products`, `locales`]
|
|
317
|
-
* @param path
|
|
318
|
-
*/
|
|
319
|
-
function fullPathToCollectionSegments(path) {
|
|
320
|
-
return path.split("/").filter((e, i) => i % 2 === 0);
|
|
321
|
-
}
|
|
322
|
-
//#endregion
|
|
323
|
-
//#region src/util/relations.ts
|
|
324
|
-
function sanitizeRelation(relation, sourceCollection, resolveCollection) {
|
|
325
|
-
if (!relation.target) throw new Error("Relation is missing a `target` collection.");
|
|
326
|
-
const rawTarget = relation.target;
|
|
327
|
-
let targetCollection;
|
|
328
|
-
if (typeof rawTarget === "string") {
|
|
329
|
-
if (resolveCollection) targetCollection = resolveCollection(rawTarget);
|
|
330
|
-
if (!targetCollection) targetCollection = {
|
|
331
|
-
slug: rawTarget,
|
|
332
|
-
name: rawTarget
|
|
333
|
-
};
|
|
334
|
-
} else if (typeof rawTarget === "function") {
|
|
335
|
-
const evaluated = rawTarget();
|
|
336
|
-
if (typeof evaluated === "string") {
|
|
337
|
-
if (resolveCollection) targetCollection = resolveCollection(evaluated);
|
|
338
|
-
if (!targetCollection) targetCollection = {
|
|
339
|
-
slug: evaluated,
|
|
340
|
-
name: evaluated
|
|
341
|
-
};
|
|
342
|
-
} else targetCollection = evaluated;
|
|
343
|
-
} else if (rawTarget && typeof rawTarget === "object") targetCollection = rawTarget;
|
|
344
|
-
if (!targetCollection) throw new Error("Relation is missing a valid `target` collection.");
|
|
345
|
-
const newRelation = { ...relation };
|
|
346
|
-
newRelation.target = () => {
|
|
347
|
-
if (typeof rawTarget === "string") return resolveCollection && resolveCollection(rawTarget) || targetCollection;
|
|
348
|
-
else if (typeof rawTarget === "function") {
|
|
349
|
-
const evaluated = rawTarget();
|
|
350
|
-
if (typeof evaluated === "string") return resolveCollection && resolveCollection(evaluated) || targetCollection;
|
|
351
|
-
return evaluated;
|
|
352
|
-
}
|
|
353
|
-
return targetCollection;
|
|
354
|
-
};
|
|
355
|
-
if (!newRelation.relationName) newRelation.relationName = (0, _rebasepro_utils.toSnakeCase)(targetCollection.slug);
|
|
356
|
-
if (!newRelation.direction) if (newRelation.foreignKeyOnTarget) newRelation.direction = "inverse";
|
|
357
|
-
else if (newRelation.through) newRelation.direction = "owning";
|
|
358
|
-
else if (newRelation.cardinality === "many") newRelation.direction = "inverse";
|
|
359
|
-
else newRelation.direction = "owning";
|
|
360
|
-
if (!newRelation.joinPath) {
|
|
361
|
-
const sourceName = (0, _rebasepro_utils.toSnakeCase)(sourceCollection.slug ?? sourceCollection.name);
|
|
362
|
-
if (newRelation.cardinality === "one" && newRelation.direction === "owning") {
|
|
363
|
-
if (!newRelation.localKey) newRelation.localKey = (0, _rebasepro_utils.generateForeignKeyName)(newRelation.relationName);
|
|
364
|
-
} else if (newRelation.cardinality === "one" && newRelation.direction === "inverse") {
|
|
365
|
-
if (!newRelation.foreignKeyOnTarget) {
|
|
366
|
-
let foundForeignKey = false;
|
|
367
|
-
try {
|
|
368
|
-
const targetRelations = (0, _rebasepro_types.getDataSourceCapabilities)(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
|
|
369
|
-
for (const targetRel of targetRelations) if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) try {
|
|
370
|
-
if (targetRel.target().slug === sourceCollection.slug) {
|
|
371
|
-
newRelation.foreignKeyOnTarget = targetRel.localKey;
|
|
372
|
-
foundForeignKey = true;
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
} catch (e) {
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
} catch (e) {}
|
|
379
|
-
if (!foundForeignKey) newRelation.foreignKeyOnTarget = (0, _rebasepro_utils.generateForeignKeyName)(newRelation.inverseRelationName ? (0, _rebasepro_utils.toSnakeCase)(newRelation.inverseRelationName) : sourceName);
|
|
380
|
-
}
|
|
381
|
-
} else if (newRelation.cardinality === "many" && newRelation.direction === "inverse") {
|
|
382
|
-
let isManyToManyInverse = false;
|
|
383
|
-
if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) try {
|
|
384
|
-
const targetRelations = (0, _rebasepro_types.getDataSourceCapabilities)(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
|
|
385
|
-
for (const targetRel of targetRelations) if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
|
|
386
|
-
isManyToManyInverse = true;
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
if (!isManyToManyInverse && targetCollection.properties) for (const [propKey, prop] of Object.entries(targetCollection.properties)) {
|
|
390
|
-
if (prop.type !== "relation") continue;
|
|
391
|
-
const relProp = prop;
|
|
392
|
-
if ((relProp.relationName || propKey) === newRelation.inverseRelationName && relProp.cardinality === "many" && (relProp.direction === "owning" || !relProp.direction)) {
|
|
393
|
-
isManyToManyInverse = true;
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
} catch (e) {}
|
|
398
|
-
if (!isManyToManyInverse && !newRelation.foreignKeyOnTarget) newRelation.foreignKeyOnTarget = (0, _rebasepro_utils.generateForeignKeyName)(sourceName);
|
|
399
|
-
} else if (newRelation.cardinality === "many" && newRelation.direction === "owning") {
|
|
400
|
-
const sourceTableName = getTableName(sourceCollection);
|
|
401
|
-
const targetTableName = getTableName(targetCollection);
|
|
402
|
-
newRelation.through = {
|
|
403
|
-
table: newRelation.through?.table ?? [sourceTableName, targetTableName].sort().join("_"),
|
|
404
|
-
sourceColumn: newRelation.through?.sourceColumn ?? (0, _rebasepro_utils.generateForeignKeyName)(sourceName),
|
|
405
|
-
targetColumn: newRelation.through?.targetColumn ?? (0, _rebasepro_utils.generateForeignKeyName)(newRelation.relationName)
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (newRelation.cardinality === "one" && newRelation.direction === "owning" && !newRelation.localKey && !newRelation.joinPath) throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'owning' one-to-one relation requires a 'localKey'. Check the relation config for '${newRelation.relationName}'`);
|
|
410
|
-
if (newRelation.cardinality === "one" && newRelation.direction === "inverse" && !newRelation.foreignKeyOnTarget && !newRelation.joinPath) throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'inverse' one-to-one relation requires a 'foreignKeyOnTarget'. Check the relation config for '${newRelation.relationName}'`);
|
|
411
|
-
if (newRelation.cardinality === "many" && newRelation.direction === "inverse" && !newRelation.foreignKeyOnTarget && !newRelation.joinPath && !newRelation.inverseRelationName) throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'inverse' one-to-many relation requires a 'foreignKeyOnTarget'. Check the relation config for '${newRelation.relationName}'`);
|
|
412
|
-
return newRelation;
|
|
413
|
-
}
|
|
414
|
-
/** WeakMap cache — same collection instance always yields the same relation map. */
|
|
415
|
-
var _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
|
|
416
|
-
function resolveCollectionRelations(collection) {
|
|
417
|
-
const cached = _resolvedRelationsCache.get(collection);
|
|
418
|
-
if (cached) return cached;
|
|
419
|
-
if (!(0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsRelations) return {};
|
|
420
|
-
const relations = {};
|
|
421
|
-
const registeredRelationNames = /* @__PURE__ */ new Set();
|
|
422
|
-
if (collection.relations) collection.relations.forEach((relation) => {
|
|
423
|
-
try {
|
|
424
|
-
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
425
|
-
const relationKey = normalizedRelation.relationName;
|
|
426
|
-
if (relationKey) {
|
|
427
|
-
relations[relationKey] = normalizedRelation;
|
|
428
|
-
registeredRelationNames.add(relationKey);
|
|
429
|
-
}
|
|
430
|
-
} catch (e) {}
|
|
431
|
-
});
|
|
432
|
-
if (collection.properties) Object.entries(collection.properties).forEach(([propKey, prop]) => {
|
|
433
|
-
const relation = resolvePropertyRelation({
|
|
434
|
-
propertyKey: propKey,
|
|
435
|
-
property: prop,
|
|
436
|
-
sourceCollection: collection
|
|
437
|
-
});
|
|
438
|
-
if (relation) {
|
|
439
|
-
if (relations[propKey]) return;
|
|
440
|
-
if (!relation.relationName) relation.relationName = propKey;
|
|
441
|
-
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
442
|
-
relations[propKey] = normalizedRelation;
|
|
443
|
-
registeredRelationNames.add(normalizedRelation.relationName ?? propKey);
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
_resolvedRelationsCache.set(collection, relations);
|
|
447
|
-
return relations;
|
|
448
|
-
}
|
|
449
|
-
function resolvePropertyRelation({ propertyKey, property, sourceCollection }) {
|
|
450
|
-
if (property.type !== "relation") return void 0;
|
|
451
|
-
const relProp = property;
|
|
452
|
-
if (relProp.target) return {
|
|
453
|
-
relationName: relProp.relationName || propertyKey,
|
|
454
|
-
target: relProp.target,
|
|
455
|
-
cardinality: relProp.cardinality || "one",
|
|
456
|
-
direction: relProp.direction || "owning",
|
|
457
|
-
inverseRelationName: relProp.inverseRelationName,
|
|
458
|
-
localKey: relProp.localKey,
|
|
459
|
-
foreignKeyOnTarget: relProp.foreignKeyOnTarget,
|
|
460
|
-
through: relProp.through,
|
|
461
|
-
joinPath: relProp.joinPath,
|
|
462
|
-
onUpdate: relProp.onUpdate,
|
|
463
|
-
onDelete: relProp.onDelete,
|
|
464
|
-
overrides: relProp.overrides
|
|
465
|
-
};
|
|
466
|
-
console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
467
|
-
}
|
|
468
|
-
function getTableName(collection) {
|
|
469
|
-
if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsRelations) return collection.table ?? (0, _rebasepro_utils.toSnakeCase)(collection.slug) ?? (0, _rebasepro_utils.toSnakeCase)(collection.name);
|
|
470
|
-
return (0, _rebasepro_utils.toSnakeCase)(collection.slug) ?? (0, _rebasepro_utils.toSnakeCase)(collection.name);
|
|
471
|
-
}
|
|
472
|
-
function getTableVarName(tableName) {
|
|
473
|
-
return tableName.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
|
|
474
|
-
}
|
|
475
|
-
function getEnumVarName(tableName, propName) {
|
|
476
|
-
return `${getTableVarName(tableName)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
|
|
477
|
-
}
|
|
478
|
-
function getColumnName(fullColumn) {
|
|
479
|
-
return fullColumn.includes(".") ? fullColumn.split(".").pop() : fullColumn;
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Look up a relation by key with forgiving normalization.
|
|
483
|
-
*
|
|
484
|
-
* `resolveCollectionRelations` stores each relation under a single canonical
|
|
485
|
-
* key (no aliases). This helper tries the given key as-is, then falls back to
|
|
486
|
-
* slug form (underscores → hyphens) and snake_case form (hyphens → underscores)
|
|
487
|
-
* so that callers that receive a key from external input (URL path segments,
|
|
488
|
-
* user-provided config, etc.) can still find the right entry.
|
|
489
|
-
*/
|
|
490
|
-
function findRelation(resolvedRelations, key) {
|
|
491
|
-
if (resolvedRelations[key]) return resolvedRelations[key];
|
|
492
|
-
const slugKey = key.replace(/_/g, "-");
|
|
493
|
-
if (slugKey !== key && resolvedRelations[slugKey]) return resolvedRelations[slugKey];
|
|
494
|
-
const snakeKey = key.replace(/-/g, "_");
|
|
495
|
-
if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
|
|
496
|
-
}
|
|
497
|
-
//#endregion
|
|
498
|
-
//#region src/util/resolutions.ts
|
|
499
|
-
function resolveProperty(props) {
|
|
500
|
-
const { property, ignoreMissingFields = false, ...rest } = props;
|
|
501
|
-
let resultProperty;
|
|
502
|
-
if (isPropertyBuilder(property)) {
|
|
503
|
-
const path = rest.path;
|
|
504
|
-
if (!path) resultProperty = property;
|
|
505
|
-
else {
|
|
506
|
-
const usedPropertyValue = rest.propertyKey ? (0, _rebasepro_utils.getIn)(rest.values, rest.propertyKey) : void 0;
|
|
507
|
-
const dynamicProps = property.dynamicProps?.({
|
|
508
|
-
...rest,
|
|
509
|
-
path,
|
|
510
|
-
propertyValue: usedPropertyValue,
|
|
511
|
-
values: rest.values ?? {},
|
|
512
|
-
previousValues: rest.previousValues ?? rest.values ?? {}
|
|
513
|
-
});
|
|
514
|
-
resultProperty = (0, _rebasepro_utils.mergeDeep)(property, dynamicProps ?? {});
|
|
515
|
-
}
|
|
516
|
-
} else resultProperty = property;
|
|
517
|
-
if (resultProperty?.dynamicProps && rest.path) {
|
|
518
|
-
const path = rest.path;
|
|
519
|
-
const usedPropertyValue = rest.propertyKey ? (0, _rebasepro_utils.getIn)(rest.values, rest.propertyKey) : void 0;
|
|
520
|
-
const dynamicPropsResult = resultProperty.dynamicProps({
|
|
521
|
-
...rest,
|
|
522
|
-
path,
|
|
523
|
-
propertyValue: usedPropertyValue,
|
|
524
|
-
values: rest.values ?? {},
|
|
525
|
-
previousValues: rest.previousValues ?? rest.values ?? {}
|
|
526
|
-
});
|
|
527
|
-
if (dynamicPropsResult) resultProperty = (0, _rebasepro_utils.mergeDeep)(resultProperty, dynamicPropsResult);
|
|
528
|
-
}
|
|
529
|
-
let resolvedProperty;
|
|
530
|
-
if (resultProperty?.type === "map" && resultProperty.properties) {
|
|
531
|
-
const properties = resolveProperties({
|
|
532
|
-
ignoreMissingFields,
|
|
533
|
-
...rest,
|
|
534
|
-
properties: resultProperty.properties
|
|
535
|
-
});
|
|
536
|
-
resolvedProperty = {
|
|
537
|
-
...resultProperty,
|
|
538
|
-
properties
|
|
539
|
-
};
|
|
540
|
-
} else if (resultProperty?.type === "array") resolvedProperty = resultProperty;
|
|
541
|
-
else if ((resultProperty?.type === "string" || resultProperty?.type === "number") && resultProperty.enum) resolvedProperty = resolvePropertyEnum(resultProperty);
|
|
542
|
-
else resolvedProperty = resultProperty;
|
|
543
|
-
if (resolvedProperty?.propertyConfig && !(0, _rebasepro_utils.isDefaultFieldConfigId)(resolvedProperty.propertyConfig)) {
|
|
544
|
-
const cmsFields = rest.propertyConfigs;
|
|
545
|
-
if (!cmsFields && !ignoreMissingFields) throw Error(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property configs were provided. Use the property 'propertyConfigs' in your app config to provide them`);
|
|
546
|
-
const customField = cmsFields?.[resolvedProperty.propertyConfig];
|
|
547
|
-
if (!customField) {
|
|
548
|
-
console.warn(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property config with that key was found. Check the 'propertyConfigs' in your app config`);
|
|
549
|
-
return resolvedProperty;
|
|
550
|
-
}
|
|
551
|
-
if (customField.property) {
|
|
552
|
-
const restConfigProperty = { ...customField.property };
|
|
553
|
-
delete restConfigProperty.propertyConfig;
|
|
554
|
-
const customFieldProperty = resolveProperty({
|
|
555
|
-
property: {
|
|
556
|
-
name: "",
|
|
557
|
-
...restConfigProperty
|
|
558
|
-
},
|
|
559
|
-
ignoreMissingFields,
|
|
560
|
-
...rest
|
|
561
|
-
});
|
|
562
|
-
if (customFieldProperty) resolvedProperty = (0, _rebasepro_utils.mergeDeep)(customFieldProperty, resolvedProperty);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return resolvedProperty;
|
|
566
|
-
}
|
|
567
|
-
function resolveRelationProperty(property, relations, propertyKey) {
|
|
568
|
-
if (property.relation) return property;
|
|
569
|
-
const name = property.relationName || propertyKey;
|
|
570
|
-
const relation = name ? relations.find((rel) => rel.relationName === name) : void 0;
|
|
571
|
-
if (!relation) throw Error(`Relation ${name ?? "(unnamed)"} not found`);
|
|
572
|
-
return {
|
|
573
|
-
...property,
|
|
574
|
-
relation
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Resolve enum aliases for a string or number property
|
|
579
|
-
* @param property
|
|
580
|
-
*/
|
|
581
|
-
function resolvePropertyEnum(property) {
|
|
582
|
-
if (typeof property.enum === "object") return {
|
|
583
|
-
...property,
|
|
584
|
-
enum: enumToObjectEntries(property.enum)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? []
|
|
585
|
-
};
|
|
586
|
-
return property;
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Resolve enums and arrays for properties
|
|
590
|
-
* @param properties
|
|
591
|
-
* @param value
|
|
592
|
-
*/
|
|
593
|
-
function resolveProperties({ propertyKey, properties, ignoreMissingFields, ...props }) {
|
|
594
|
-
return Object.entries(properties).map(([key, property]) => {
|
|
595
|
-
const childResolvedProperty = resolveProperty({
|
|
596
|
-
propertyKey: propertyKey ? `${propertyKey}.${key}` : void 0,
|
|
597
|
-
property,
|
|
598
|
-
ignoreMissingFields,
|
|
599
|
-
...props
|
|
600
|
-
});
|
|
601
|
-
if (!childResolvedProperty) return {};
|
|
602
|
-
return { [key]: childResolvedProperty };
|
|
603
|
-
}).filter((a) => a !== null).reduce((a, b) => ({
|
|
604
|
-
...a,
|
|
605
|
-
...b
|
|
606
|
-
}), {});
|
|
607
|
-
}
|
|
608
|
-
function resolveArrayProperties({ propertyKey, property, ignoreMissingFields = false, ...props }) {
|
|
609
|
-
const propertyValue = propertyKey ? (0, _rebasepro_utils.getIn)(props.values, propertyKey) : void 0;
|
|
610
|
-
if (property.of) if (Array.isArray(property.of)) return property.of.map((p, index) => {
|
|
611
|
-
return resolveProperty({
|
|
612
|
-
propertyKey: `${propertyKey}.${index}`,
|
|
613
|
-
property: p,
|
|
614
|
-
ignoreMissingFields,
|
|
615
|
-
...props,
|
|
616
|
-
index
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
else {
|
|
620
|
-
const of = property.of;
|
|
621
|
-
const resolvedProperties = getArrayResolvedProperties({
|
|
622
|
-
propertyValue,
|
|
623
|
-
propertyKey,
|
|
624
|
-
property,
|
|
625
|
-
ignoreMissingFields,
|
|
626
|
-
...props
|
|
627
|
-
});
|
|
628
|
-
const { values, previousValues, ...rest } = props;
|
|
629
|
-
if (!resolveProperty({
|
|
630
|
-
property: of,
|
|
631
|
-
ignoreMissingFields,
|
|
632
|
-
...rest
|
|
633
|
-
}) && !ignoreMissingFields) throw Error("When using a property builder as the 'of' prop of an ArrayProperty, you must return a valid child property");
|
|
634
|
-
return resolvedProperties;
|
|
635
|
-
}
|
|
636
|
-
else if (property.oneOf) {
|
|
637
|
-
const typeField = property.oneOf?.typeField ?? "type";
|
|
638
|
-
return Array.isArray(propertyValue) ? propertyValue.map((v, index) => {
|
|
639
|
-
const type = v && v[typeField];
|
|
640
|
-
const childProperty = property.oneOf?.properties[type];
|
|
641
|
-
if (!type || !childProperty) return null;
|
|
642
|
-
return resolveProperty({
|
|
643
|
-
propertyKey: `${propertyKey}.${index}`,
|
|
644
|
-
property: childProperty,
|
|
645
|
-
ignoreMissingFields,
|
|
646
|
-
...props
|
|
647
|
-
});
|
|
648
|
-
}).filter((e) => Boolean(e)) : [];
|
|
649
|
-
} else if (!("Field" in (property.ui || {}) && property.ui?.Field)) throw Error(`The array property (${propertyKey}) needs to declare an 'of' or a 'oneOf' property, or provide a custom \`Field\` component`);
|
|
650
|
-
else return [];
|
|
651
|
-
}
|
|
652
|
-
function getArrayResolvedProperties({ propertyKey, propertyValue, property, ...props }) {
|
|
653
|
-
const of = property.of;
|
|
654
|
-
if (!of) throw Error(`Trying to resolve an array property (${propertyKey}) without providing an 'of' property`);
|
|
655
|
-
return Array.isArray(propertyValue) ? propertyValue.map((v, index) => {
|
|
656
|
-
return resolveProperty({
|
|
657
|
-
propertyKey: `${propertyKey}.${index}`,
|
|
658
|
-
property: Array.isArray(of) ? of[index] : of,
|
|
659
|
-
...props,
|
|
660
|
-
index
|
|
661
|
-
});
|
|
662
|
-
}).filter((e) => Boolean(e)) : [];
|
|
663
|
-
}
|
|
664
|
-
function resolveEnumValues(input) {
|
|
665
|
-
if (typeof input === "object") return Object.entries(input).map(([id, value]) => typeof value === "string" ? {
|
|
666
|
-
id,
|
|
667
|
-
label: value
|
|
668
|
-
} : value);
|
|
669
|
-
else if (Array.isArray(input)) return input;
|
|
670
|
-
else return;
|
|
671
|
-
}
|
|
672
|
-
function getSubcollections(collection) {
|
|
673
|
-
if (collection.childCollections) return collection.childCollections() ?? [];
|
|
674
|
-
const declaredSubcollections = (0, _rebasepro_types.getDeclaredSubcollections)(collection);
|
|
675
|
-
if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsSubcollections && declaredSubcollections) return declaredSubcollections() ?? [];
|
|
676
|
-
if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsRelations) {
|
|
677
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
678
|
-
return Object.values(resolvedRelations).filter((r) => r.cardinality === "many").map((r) => {
|
|
679
|
-
const target = r.target();
|
|
680
|
-
if (!target) return void 0;
|
|
681
|
-
const relationKey = r.relationName || target.slug;
|
|
682
|
-
let customName;
|
|
683
|
-
if (collection.properties) {
|
|
684
|
-
const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
|
|
685
|
-
if (prop && prop[1].name) customName = prop[1].name;
|
|
686
|
-
}
|
|
687
|
-
const baseOverrides = { slug: relationKey };
|
|
688
|
-
if (customName) {
|
|
689
|
-
baseOverrides.name = customName;
|
|
690
|
-
baseOverrides.singularName = customName;
|
|
691
|
-
}
|
|
692
|
-
const targetWithOverrides = {
|
|
693
|
-
...target,
|
|
694
|
-
...baseOverrides
|
|
695
|
-
};
|
|
696
|
-
return r.overrides ? (0, _rebasepro_utils.mergeDeep)(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
697
|
-
}).filter((c) => Boolean(c));
|
|
698
|
-
}
|
|
699
|
-
return [];
|
|
700
|
-
}
|
|
701
|
-
//#endregion
|
|
702
|
-
//#region src/util/policy/sqlToPolicy.ts
|
|
703
|
-
/**
|
|
704
|
-
* A tiny, regex-based SQL "parser" for security rules.
|
|
705
|
-
*
|
|
706
|
-
* This is NOT a full SQL parser. It is designed to handle the subset of SQL
|
|
707
|
-
* commonly used in `USING` and `WITH CHECK` clauses, enough to drive the
|
|
708
|
-
* optimistic client-side UI decision.
|
|
709
|
-
*
|
|
710
|
-
* It handles:
|
|
711
|
-
* - `field = 'literal'`
|
|
712
|
-
* - `field != 'literal'`
|
|
713
|
-
* - `field = current_setting('app.user_id')`
|
|
714
|
-
* - `A AND B`
|
|
715
|
-
* - `true`
|
|
716
|
-
* - `IN (...)` (as optimistic true)
|
|
717
|
-
*
|
|
718
|
-
* For anything it doesn't understand, it returns a `raw` expression, which
|
|
719
|
-
* the evaluator treats as "unknown" (and usually optimistic true).
|
|
720
|
-
*/
|
|
721
|
-
function sqlToPolicy(sql) {
|
|
722
|
-
const trimmed = sql.trim();
|
|
723
|
-
if (trimmed.toLowerCase() === "true") return _rebasepro_types.policy.true();
|
|
724
|
-
if (trimmed.toLowerCase() === "false") return _rebasepro_types.policy.false();
|
|
725
|
-
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
|
|
726
|
-
if (overlapMatch) {
|
|
727
|
-
const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
728
|
-
return _rebasepro_types.policy.rolesOverlap(roles);
|
|
729
|
-
}
|
|
730
|
-
const containMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
|
|
731
|
-
if (containMatch) {
|
|
732
|
-
const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
733
|
-
return _rebasepro_types.policy.rolesContain(roles);
|
|
734
|
-
}
|
|
735
|
-
if (trimmed.toUpperCase().includes(" OR ")) {
|
|
736
|
-
const parts = trimmed.split(/ OR /i);
|
|
737
|
-
return _rebasepro_types.policy.or(...parts.map(sqlToPolicy));
|
|
738
|
-
}
|
|
739
|
-
if (trimmed.toUpperCase().includes(" AND ")) {
|
|
740
|
-
const parts = trimmed.split(/ AND /i);
|
|
741
|
-
return _rebasepro_types.policy.and(...parts.map(sqlToPolicy));
|
|
742
|
-
}
|
|
743
|
-
const match = trimmed.match(/^(.+?)\s*(!?=)\s*(.+)$/);
|
|
744
|
-
if (match) {
|
|
745
|
-
const [, leftStr, op, rightStr] = match;
|
|
746
|
-
const left = parseOperand(leftStr.trim());
|
|
747
|
-
const right = parseOperand(rightStr.trim());
|
|
748
|
-
if (left && right) return _rebasepro_types.policy.compare(left, op === "=" ? "eq" : "neq", right);
|
|
749
|
-
}
|
|
750
|
-
return _rebasepro_types.policy.raw(sql);
|
|
751
|
-
}
|
|
752
|
-
function parseOperand(str) {
|
|
753
|
-
if (/current_setting\s*\(\s*'app\.user_id'\s*\)/i.test(str) || /auth\.uid\(\)/i.test(str)) return _rebasepro_types.policy.authUid();
|
|
754
|
-
const stringMatch = str.match(/^'(.+)'$/);
|
|
755
|
-
if (stringMatch) return _rebasepro_types.policy.literal(stringMatch[1]);
|
|
756
|
-
if (/^\w+$/.test(str)) return _rebasepro_types.policy.field(str);
|
|
757
|
-
return null;
|
|
758
|
-
}
|
|
759
|
-
//#endregion
|
|
760
|
-
//#region src/util/policy/securityRuleToConditions.ts
|
|
761
|
-
/**
|
|
762
|
-
* Desugars a {@link SecurityRule} — its `access`/`ownerField`/`roles` shortcuts,
|
|
763
|
-
* structured `condition`/`check`, and raw `using`/`withCheck` — into a single
|
|
764
|
-
* normalized {@link PolicyExpression} pair.
|
|
765
|
-
*
|
|
766
|
-
* **This is the linchpin against drift:** both the Postgres DDL generators and
|
|
767
|
-
* the client-side evaluator consume this one function, so there is exactly one
|
|
768
|
-
* definition of what a rule means. In particular, application `roles` are folded
|
|
769
|
-
* into the expression here (AND'd with the base condition, matching how Postgres
|
|
770
|
-
* generates the clause) rather than being handled separately by each consumer.
|
|
771
|
-
*/
|
|
772
|
-
function securityRuleToConditions(rule) {
|
|
773
|
-
return {
|
|
774
|
-
usingExpr: withRoles(baseUsing(rule), rule),
|
|
775
|
-
withCheckExpr: withRoles(baseWithCheck(rule), rule)
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
function baseUsing(rule) {
|
|
779
|
-
if (rule.condition) return rule.condition;
|
|
780
|
-
if (rule.using != null) return sqlToPolicy(rule.using);
|
|
781
|
-
if (rule.access === "public") return _rebasepro_types.policy.true();
|
|
782
|
-
if (rule.ownerField) return _rebasepro_types.policy.compare(_rebasepro_types.policy.field(rule.ownerField), "eq", _rebasepro_types.policy.authUid());
|
|
783
|
-
return null;
|
|
784
|
-
}
|
|
785
|
-
function baseWithCheck(rule) {
|
|
786
|
-
if (rule.check) return rule.check;
|
|
787
|
-
if (rule.withCheck != null) return sqlToPolicy(rule.withCheck);
|
|
788
|
-
return baseUsing(rule);
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* AND the base condition with an application-role check, or produce a roles-only
|
|
792
|
-
* condition when there is no base. Mirrors the Postgres generator so that a
|
|
793
|
-
* role-scoped restrictive rule denies exactly the same set of users on both
|
|
794
|
-
* sides.
|
|
795
|
-
*/
|
|
796
|
-
function withRoles(base, rule) {
|
|
797
|
-
if (!rule.roles || rule.roles.length === 0) return base;
|
|
798
|
-
const rolesExpr = _rebasepro_types.policy.rolesOverlap(rule.roles);
|
|
799
|
-
if (rule.mode === "restrictive") return base ? _rebasepro_types.policy.or(_rebasepro_types.policy.not(rolesExpr), base) : _rebasepro_types.policy.not(rolesExpr);
|
|
800
|
-
return base ? _rebasepro_types.policy.and(base, rolesExpr) : rolesExpr;
|
|
801
|
-
}
|
|
802
|
-
//#endregion
|
|
803
|
-
//#region src/util/policy/policyToPostgres.ts
|
|
804
|
-
/**
|
|
805
|
-
* Compiles a {@link PolicyExpression} to a PostgreSQL boolean SQL string,
|
|
806
|
-
* suitable for a `USING (...)` / `WITH CHECK (...)` clause.
|
|
807
|
-
*
|
|
808
|
-
* This is one of the two consumers of the shared policy model (the other being
|
|
809
|
-
* {@link evaluatePolicy}); the Postgres schema generators call it so that DDL
|
|
810
|
-
* and the admin UI derive from the exact same expression.
|
|
811
|
-
*/
|
|
812
|
-
function policyToPostgres(expr, collection, options) {
|
|
813
|
-
return compile(expr, {
|
|
814
|
-
fieldCollection: collection,
|
|
815
|
-
fieldPrefix: "",
|
|
816
|
-
outerCollection: collection,
|
|
817
|
-
outerPrefix: "",
|
|
818
|
-
resolveCollection: options?.resolveCollection,
|
|
819
|
-
alias: { n: 0 }
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
function compile(expr, scope) {
|
|
823
|
-
switch (expr.kind) {
|
|
824
|
-
case "true": return "true";
|
|
825
|
-
case "false": return "false";
|
|
826
|
-
case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" AND ");
|
|
827
|
-
case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" OR ");
|
|
828
|
-
case "not":
|
|
829
|
-
if (expr.operand.kind === "authenticated") return "auth.uid() IS NULL";
|
|
830
|
-
return `NOT (${compile(expr.operand, scope)})`;
|
|
831
|
-
case "compare": {
|
|
832
|
-
const castForAuthUid = (operand, sqlText, other) => other.kind === "authUid" && (operand.kind === "field" || operand.kind === "outerField") ? `(${sqlText})::text` : sqlText;
|
|
833
|
-
const leftSql = castForAuthUid(expr.left, operandToSql(expr.left, scope), expr.right);
|
|
834
|
-
const rightSql = castForAuthUid(expr.right, operandToSql(expr.right, scope), expr.left);
|
|
835
|
-
return `${leftSql} ${COMPARE_SQL[expr.op]} ${rightSql}`;
|
|
836
|
-
}
|
|
837
|
-
case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
|
|
838
|
-
case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
|
|
839
|
-
case "authenticated": return "auth.uid() IS NOT NULL";
|
|
840
|
-
case "existsIn": return compileExistsIn(expr, scope);
|
|
841
|
-
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* Compiles `existsIn` to a correlated `EXISTS (SELECT 1 FROM <join> WHERE ...)`.
|
|
846
|
-
* Inside the subquery, `field` operands bind to the aliased join table and
|
|
847
|
-
* `outerField` operands bind to the (table-qualified) outer RLS row.
|
|
848
|
-
*/
|
|
849
|
-
function compileExistsIn(expr, scope) {
|
|
850
|
-
const join = scope.resolveCollection?.(expr.collection);
|
|
851
|
-
const joinTable = join ? getTableName(join) : (0, _rebasepro_utils.toSnakeCase)(expr.collection);
|
|
852
|
-
const joinSchema = schemaOf(join) ?? schemaOf(scope.outerCollection) ?? "public";
|
|
853
|
-
const alias = `_ex${scope.alias.n++}`;
|
|
854
|
-
const outerPrefix = outerQualifier(scope);
|
|
855
|
-
const innerScope = {
|
|
856
|
-
fieldCollection: join,
|
|
857
|
-
fieldPrefix: `"${alias}".`,
|
|
858
|
-
outerCollection: scope.outerCollection,
|
|
859
|
-
outerPrefix,
|
|
860
|
-
resolveCollection: scope.resolveCollection,
|
|
861
|
-
alias: scope.alias
|
|
862
|
-
};
|
|
863
|
-
return `EXISTS (SELECT 1 FROM "${joinSchema}"."${joinTable}" "${alias}" WHERE ${compile(expr.where, innerScope)})`;
|
|
864
|
-
}
|
|
865
|
-
var COMPARE_SQL = {
|
|
866
|
-
eq: "=",
|
|
867
|
-
neq: "!=",
|
|
868
|
-
lt: "<",
|
|
869
|
-
lte: "<=",
|
|
870
|
-
gt: ">",
|
|
871
|
-
gte: ">="
|
|
872
|
-
};
|
|
873
|
-
function operandToSql(operand, scope) {
|
|
874
|
-
switch (operand.kind) {
|
|
875
|
-
case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
|
|
876
|
-
case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
|
|
877
|
-
case "literal": return quoteLiteral(operand.value);
|
|
878
|
-
case "authUid": return "auth.uid()";
|
|
879
|
-
case "authRoles": return "string_to_array(auth.roles(), ',')";
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* SQL prefix that qualifies a column of the outer RLS row (`"schema"."table".`),
|
|
884
|
-
* or `""` when the collection is unknown.
|
|
885
|
-
*/
|
|
886
|
-
function outerQualifier(scope) {
|
|
887
|
-
const table = scope.outerCollection ? getTableName(scope.outerCollection) : void 0;
|
|
888
|
-
if (!table) return "";
|
|
889
|
-
return `"${schemaOf(scope.outerCollection) ?? "public"}"."${table}".`;
|
|
890
|
-
}
|
|
891
|
-
function schemaOf(collection) {
|
|
892
|
-
return collection?.schema || void 0;
|
|
893
|
-
}
|
|
894
|
-
function resolveColumnName(propName, collection) {
|
|
895
|
-
const prop = collection?.properties?.[propName];
|
|
896
|
-
if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
|
|
897
|
-
return (0, _rebasepro_utils.toSnakeCase)(propName);
|
|
898
|
-
}
|
|
899
|
-
function quoteLiteral(value) {
|
|
900
|
-
if (value === null) return "NULL";
|
|
901
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
902
|
-
if (typeof value === "number") return String(value);
|
|
903
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
904
|
-
}
|
|
905
|
-
/** Sorted, single-quoted `ARRAY['a','b']` — matches the generators' output. */
|
|
906
|
-
function rolesArraySql(roles) {
|
|
907
|
-
return `ARRAY[${[...roles].sort().map((r) => `'${r}'`).join(",")}]`;
|
|
908
|
-
}
|
|
909
|
-
//#endregion
|
|
910
|
-
//#region src/util/policy/evaluatePolicy.ts
|
|
911
|
-
/**
|
|
912
|
-
* Evaluates a {@link PolicyExpression} against a user + row, using three-valued
|
|
913
|
-
* (Kleene) logic so that `"unknown"` sub-results propagate soundly.
|
|
914
|
-
*
|
|
915
|
-
* This is the JavaScript twin of {@link policyToPostgres}: both derive from the
|
|
916
|
-
* same expression, so the admin UI matches database enforcement by construction
|
|
917
|
-
* for every non-raw rule.
|
|
918
|
-
*/
|
|
919
|
-
function evaluatePolicy(expr, ctx) {
|
|
920
|
-
switch (expr.kind) {
|
|
921
|
-
case "true": return true;
|
|
922
|
-
case "false": return false;
|
|
923
|
-
case "and": return kleeneAnd$1(expr.operands.map((o) => evaluatePolicy(o, ctx)));
|
|
924
|
-
case "or": return kleeneOr(expr.operands.map((o) => evaluatePolicy(o, ctx)));
|
|
925
|
-
case "not": return kleeneNot(evaluatePolicy(expr.operand, ctx));
|
|
926
|
-
case "compare": return evaluateCompare(expr.op, expr.left, expr.right, ctx);
|
|
927
|
-
case "rolesOverlap": {
|
|
928
|
-
const userRoles = ctx.roles ?? [];
|
|
929
|
-
return expr.roles.some((r) => r === "public" || userRoles.includes(r));
|
|
930
|
-
}
|
|
931
|
-
case "rolesContain": {
|
|
932
|
-
const userRoles = ctx.roles ?? [];
|
|
933
|
-
return expr.roles.every((r) => r === "public" || userRoles.includes(r));
|
|
934
|
-
}
|
|
935
|
-
case "authenticated": return ctx.uid != null;
|
|
936
|
-
case "existsIn": return "unknown";
|
|
937
|
-
case "raw": return "unknown";
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
function kleeneAnd$1(values) {
|
|
941
|
-
if (values.some((v) => v === false)) return false;
|
|
942
|
-
if (values.some((v) => v === "unknown")) return "unknown";
|
|
943
|
-
return true;
|
|
944
|
-
}
|
|
945
|
-
function kleeneOr(values) {
|
|
946
|
-
if (values.some((v) => v === true)) return true;
|
|
947
|
-
if (values.some((v) => v === "unknown")) return "unknown";
|
|
948
|
-
return false;
|
|
949
|
-
}
|
|
950
|
-
function kleeneNot(value) {
|
|
951
|
-
if (value === "unknown") return "unknown";
|
|
952
|
-
return !value;
|
|
953
|
-
}
|
|
954
|
-
function resolveOperand(operand, ctx) {
|
|
955
|
-
switch (operand.kind) {
|
|
956
|
-
case "literal": return {
|
|
957
|
-
known: true,
|
|
958
|
-
value: operand.value
|
|
959
|
-
};
|
|
960
|
-
case "authUid": return {
|
|
961
|
-
known: true,
|
|
962
|
-
value: ctx.uid ?? null
|
|
963
|
-
};
|
|
964
|
-
case "authRoles": return {
|
|
965
|
-
known: true,
|
|
966
|
-
value: ctx.roles ?? []
|
|
967
|
-
};
|
|
968
|
-
case "field":
|
|
969
|
-
if (!ctx.entity) return { known: false };
|
|
970
|
-
return {
|
|
971
|
-
known: true,
|
|
972
|
-
value: ctx.entity.values[operand.name]
|
|
973
|
-
};
|
|
974
|
-
case "outerField": return { known: false };
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
function evaluateCompare(op, left, right, ctx) {
|
|
978
|
-
const l = resolveOperand(left, ctx);
|
|
979
|
-
const r = resolveOperand(right, ctx);
|
|
980
|
-
if (!l.known || !r.known) return "unknown";
|
|
981
|
-
const a = l.value;
|
|
982
|
-
const b = r.value;
|
|
983
|
-
if (a === null || b === null) {
|
|
984
|
-
if (op === "eq") return false;
|
|
985
|
-
if (op === "neq") return true;
|
|
986
|
-
return "unknown";
|
|
987
|
-
}
|
|
988
|
-
if (op === "eq") return a === b;
|
|
989
|
-
if (op === "neq") return a !== b;
|
|
990
|
-
if (typeof a === "string" && typeof b === "string") {
|
|
991
|
-
if (op === "lt") return a < b;
|
|
992
|
-
if (op === "lte") return a <= b;
|
|
993
|
-
if (op === "gt") return a > b;
|
|
994
|
-
if (op === "gte") return a >= b;
|
|
995
|
-
}
|
|
996
|
-
if (typeof a === "number" && typeof b === "number") {
|
|
997
|
-
if (op === "lt") return a < b;
|
|
998
|
-
if (op === "lte") return a <= b;
|
|
999
|
-
if (op === "gt") return a > b;
|
|
1000
|
-
if (op === "gte") return a >= b;
|
|
1001
|
-
}
|
|
1002
|
-
if (typeof a === "bigint" && typeof b === "bigint") {
|
|
1003
|
-
if (op === "lt") return a < b;
|
|
1004
|
-
if (op === "lte") return a <= b;
|
|
1005
|
-
if (op === "gt") return a > b;
|
|
1006
|
-
if (op === "gte") return a >= b;
|
|
1007
|
-
}
|
|
1008
|
-
return "unknown";
|
|
1009
|
-
}
|
|
1010
|
-
//#endregion
|
|
1011
|
-
//#region src/util/permissions.ts
|
|
1012
|
-
/** Combine clause results with AND under three-valued (Kleene) logic. */
|
|
1013
|
-
function kleeneAnd(values) {
|
|
1014
|
-
if (values.some((v) => v === false)) return false;
|
|
1015
|
-
if (values.some((v) => v === "unknown")) return "unknown";
|
|
1016
|
-
return true;
|
|
1017
|
-
}
|
|
1018
|
-
/** The operations a rule covers, mirroring the Postgres generator's resolution. */
|
|
1019
|
-
function ruleOperations(rule) {
|
|
1020
|
-
return rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
1021
|
-
}
|
|
1022
|
-
function ruleApplies(rule, targetOperation) {
|
|
1023
|
-
const ops = ruleOperations(rule);
|
|
1024
|
-
return ops.includes(targetOperation) || ops.includes("all");
|
|
1025
|
-
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Evaluate a single rule for one operation, returning a tri-state.
|
|
1028
|
-
*
|
|
1029
|
-
* A `null` clause (the rule contributes no condition for a required clause)
|
|
1030
|
-
* denies — matching Postgres, which emits `USING (false)` / `WITH CHECK (false)`
|
|
1031
|
-
* in that case. USING applies to SELECT/UPDATE/DELETE; WITH CHECK to
|
|
1032
|
-
* INSERT/UPDATE; both must pass for UPDATE.
|
|
1033
|
-
*/
|
|
1034
|
-
function evaluateRuleForOperation(rule, ctx, targetOperation) {
|
|
1035
|
-
const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
|
|
1036
|
-
const clause = (expr) => expr === null ? false : evaluatePolicy(expr, ctx);
|
|
1037
|
-
const needsUsing = targetOperation !== "insert";
|
|
1038
|
-
const needsWithCheck = targetOperation === "insert" || targetOperation === "update";
|
|
1039
|
-
const results = [];
|
|
1040
|
-
if (needsUsing) results.push(clause(usingExpr));
|
|
1041
|
-
if (needsWithCheck) results.push(clause(withCheckExpr));
|
|
1042
|
-
return kleeneAnd(results);
|
|
1043
|
-
}
|
|
1044
|
-
function resolveTriState(value, onUnknown) {
|
|
1045
|
-
if (value === "unknown") return onUnknown === "allow";
|
|
1046
|
-
return value;
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Decide whether an operation is permitted for a user on a (possibly null) row,
|
|
1050
|
-
* by evaluating the collection's security rules with the shared policy model —
|
|
1051
|
-
* the same model compiled to Postgres RLS DDL, so the decision matches database
|
|
1052
|
-
* enforcement for every non-raw rule.
|
|
1053
|
-
*
|
|
1054
|
-
* @param options.onUnknown how to treat rules that cannot be decided
|
|
1055
|
-
* client-side (raw SQL, or row predicates with no row). Defaults to `"allow"`
|
|
1056
|
-
* for optimistic UI gating; enforcement callers should pass `"deny"`.
|
|
1057
|
-
*/
|
|
1058
|
-
function checkOperation(collection, authContext, entity, targetOperation, options) {
|
|
1059
|
-
const onUnknown = options?.onUnknown ?? "allow";
|
|
1060
|
-
const securityRules = (0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsRLS ? collection.securityRules : void 0;
|
|
1061
|
-
if (!securityRules || securityRules.length === 0) return true;
|
|
1062
|
-
const applicableRules = securityRules.filter((r) => ruleApplies(r, targetOperation));
|
|
1063
|
-
if (applicableRules.length === 0) return false;
|
|
1064
|
-
const ctx = {
|
|
1065
|
-
uid: authContext.user?.uid,
|
|
1066
|
-
roles: authContext.user?.roles ?? [],
|
|
1067
|
-
entity
|
|
1068
|
-
};
|
|
1069
|
-
let grantedByPermissive = false;
|
|
1070
|
-
let deniedByRestrictive = false;
|
|
1071
|
-
let hasPermissive = false;
|
|
1072
|
-
for (const rule of applicableRules) {
|
|
1073
|
-
const mode = rule.mode || "permissive";
|
|
1074
|
-
const passed = resolveTriState(evaluateRuleForOperation(rule, ctx, targetOperation), onUnknown);
|
|
1075
|
-
if (mode === "restrictive") {
|
|
1076
|
-
if (!passed) {
|
|
1077
|
-
deniedByRestrictive = true;
|
|
1078
|
-
break;
|
|
1079
|
-
}
|
|
1080
|
-
} else {
|
|
1081
|
-
hasPermissive = true;
|
|
1082
|
-
if (passed) grantedByPermissive = true;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
if (deniedByRestrictive) return false;
|
|
1086
|
-
return hasPermissive ? grantedByPermissive : false;
|
|
1087
|
-
}
|
|
1088
|
-
function canReadCollection(collection, authContext) {
|
|
1089
|
-
return checkOperation(collection, authContext, null, "select");
|
|
1090
|
-
}
|
|
1091
|
-
function canEditEntity(collection, authContext, path, entity) {
|
|
1092
|
-
return checkOperation(collection, authContext, entity, "update");
|
|
1093
|
-
}
|
|
1094
|
-
function canCreateEntity(collection, authContext, path, entity) {
|
|
1095
|
-
return checkOperation(collection, authContext, entity, "insert");
|
|
1096
|
-
}
|
|
1097
|
-
function canDeleteEntity(collection, authContext, path, entity) {
|
|
1098
|
-
return checkOperation(collection, authContext, entity, "delete");
|
|
1099
|
-
}
|
|
1100
|
-
//#endregion
|
|
1101
|
-
//#region src/util/references.ts
|
|
1102
|
-
function getEntityImagePreviewPropertyKey(collection) {
|
|
1103
|
-
for (const key in collection.properties) {
|
|
1104
|
-
const property = collection.properties[key];
|
|
1105
|
-
if (property.type === "string" && property.storage?.acceptedFiles?.includes("image/*")) return key;
|
|
1106
|
-
}
|
|
1107
|
-
for (const key in collection.properties) {
|
|
1108
|
-
const property = collection.properties[key];
|
|
1109
|
-
if (property.type === "array" && !Array.isArray(property.of) && property.of?.type === "string" && property.of.storage?.acceptedFiles?.includes("image/*")) return key;
|
|
1110
|
-
}
|
|
1111
|
-
for (const key in collection.properties) {
|
|
1112
|
-
const property = collection.properties[key];
|
|
1113
|
-
if (property.type === "string" && property.ui?.url === "image") return key;
|
|
1114
|
-
}
|
|
1115
|
-
for (const key in collection.properties) {
|
|
1116
|
-
const property = collection.properties[key];
|
|
1117
|
-
if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.ui?.url === "image") return key;
|
|
1118
|
-
}
|
|
1119
|
-
for (const key in collection.properties) {
|
|
1120
|
-
const property = collection.properties[key];
|
|
1121
|
-
if (property.type === "string" && property.storage && !property.storage.acceptedFiles) return key;
|
|
1122
|
-
}
|
|
1123
|
-
for (const key in collection.properties) {
|
|
1124
|
-
const property = collection.properties[key];
|
|
1125
|
-
if (property.type === "array" && !Array.isArray(property.of) && property.of?.type === "string" && property.of.storage && !property.of.storage.acceptedFiles) return key;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
//#endregion
|
|
1129
|
-
//#region src/util/navigation_utils.ts
|
|
1130
|
-
function removeInitialAndTrailingSlashes(s) {
|
|
1131
|
-
return removeInitialSlash(removeTrailingSlash(s));
|
|
1132
|
-
}
|
|
1133
|
-
function removeInitialSlash(s) {
|
|
1134
|
-
if (s.startsWith("/")) return s.slice(1);
|
|
1135
|
-
else return s;
|
|
1136
|
-
}
|
|
1137
|
-
function removeTrailingSlash(s) {
|
|
1138
|
-
if (s.endsWith("/")) return s.slice(0, -1);
|
|
1139
|
-
else return s;
|
|
1140
|
-
}
|
|
1141
|
-
function addInitialSlash(s) {
|
|
1142
|
-
if (s.startsWith("/")) return s;
|
|
1143
|
-
else return `/${s}`;
|
|
1144
|
-
}
|
|
1145
|
-
function getLastSegment(path) {
|
|
1146
|
-
const cleanPath = removeInitialAndTrailingSlashes(path);
|
|
1147
|
-
if (cleanPath.includes("/")) {
|
|
1148
|
-
const segments = cleanPath.split("/");
|
|
1149
|
-
return segments[segments.length - 1];
|
|
1150
|
-
}
|
|
1151
|
-
return cleanPath;
|
|
1152
|
-
}
|
|
1153
|
-
function resolveCollectionPathIds(path, allCollections) {
|
|
1154
|
-
let remainingPath = removeInitialAndTrailingSlashes(path);
|
|
1155
|
-
if (!remainingPath) return "";
|
|
1156
|
-
let currentCollections = allCollections;
|
|
1157
|
-
const resolvedPathParts = [];
|
|
1158
|
-
while (remainingPath.length > 0) {
|
|
1159
|
-
if (!currentCollections || currentCollections.length === 0) {
|
|
1160
|
-
console.warn(`resolveCollectionPathIds: Path structure implies subcollections, but none found before segment starting with "${remainingPath}" in original path "${path}". Appending remaining original path.`);
|
|
1161
|
-
resolvedPathParts.push(remainingPath);
|
|
1162
|
-
remainingPath = "";
|
|
1163
|
-
break;
|
|
1164
|
-
}
|
|
1165
|
-
let foundMatch = false;
|
|
1166
|
-
const potentialMatches = currentCollections.flatMap((col) => [{
|
|
1167
|
-
col,
|
|
1168
|
-
match: col.slug
|
|
1169
|
-
}]).filter((p) => p.match && remainingPath.startsWith(p.match)).sort((a, b) => b.match.length - a.match.length);
|
|
1170
|
-
if (potentialMatches.length > 0) {
|
|
1171
|
-
const { col: foundCollection, match: matchString } = potentialMatches[0];
|
|
1172
|
-
resolvedPathParts.push(foundCollection.slug);
|
|
1173
|
-
remainingPath = removeInitialSlash(remainingPath.substring(matchString.length));
|
|
1174
|
-
if (remainingPath.length === 0) {
|
|
1175
|
-
foundMatch = true;
|
|
1176
|
-
break;
|
|
1177
|
-
}
|
|
1178
|
-
const idSeparatorIndex = remainingPath.indexOf("/");
|
|
1179
|
-
let entityId;
|
|
1180
|
-
if (idSeparatorIndex > -1) {
|
|
1181
|
-
entityId = remainingPath.substring(0, idSeparatorIndex);
|
|
1182
|
-
remainingPath = remainingPath.substring(idSeparatorIndex + 1);
|
|
1183
|
-
} else {
|
|
1184
|
-
entityId = remainingPath;
|
|
1185
|
-
remainingPath = "";
|
|
1186
|
-
console.warn(`resolveCollectionPathIds: Path seems to end with a entity ID "${entityId}" instead of a collection segment in original path "${path}". This might indicate an invalid input path.`);
|
|
1187
|
-
}
|
|
1188
|
-
resolvedPathParts.push(entityId);
|
|
1189
|
-
currentCollections = getSubcollections(foundCollection);
|
|
1190
|
-
foundMatch = true;
|
|
1191
|
-
if (!currentCollections && remainingPath.length > 0) {
|
|
1192
|
-
console.warn(`resolveCollectionPathIds: Path continues after entity ID "${entityId}", but no subcollections are defined for the preceding collection "${foundCollection.slug}" in path "${path}". Appending remaining original path.`);
|
|
1193
|
-
resolvedPathParts.push(remainingPath);
|
|
1194
|
-
remainingPath = "";
|
|
1195
|
-
break;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
if (!foundMatch) {
|
|
1199
|
-
console.warn(`resolveCollectionPathIds: Collection definition not found for segment starting with "${remainingPath}" in original path "${path}". Appending remaining original path.`);
|
|
1200
|
-
resolvedPathParts.push(remainingPath);
|
|
1201
|
-
remainingPath = "";
|
|
1202
|
-
break;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
return resolvedPathParts.join("/");
|
|
1206
|
-
}
|
|
1207
|
-
/**
|
|
1208
|
-
* Find the corresponding view at any depth for a given path.
|
|
1209
|
-
* Note that path or segments of the paths can be collection aliases.
|
|
1210
|
-
* @param slugOrPath
|
|
1211
|
-
* @param collections
|
|
1212
|
-
*/
|
|
1213
|
-
function getCollectionBySlugWithin(slugOrPath, collections) {
|
|
1214
|
-
const subpaths = removeInitialAndTrailingSlashes(slugOrPath).split("/");
|
|
1215
|
-
if (subpaths.length % 2 === 0) throw Error(`getCollectionBySlug: Collection paths must have an odd number of segments: ${slugOrPath}`);
|
|
1216
|
-
const subpathCombinations = getCollectionPathsCombinations(subpaths);
|
|
1217
|
-
let result;
|
|
1218
|
-
for (let i = 0; i < subpathCombinations.length; i++) {
|
|
1219
|
-
const subpathCombination = subpathCombinations[i];
|
|
1220
|
-
const navigationEntry = collections && collections.sort((a, b) => (a.slug ?? "").localeCompare(b.slug ?? "")).find((entry) => entry.slug === subpathCombination);
|
|
1221
|
-
if (navigationEntry) {
|
|
1222
|
-
if (subpathCombination === slugOrPath) result = navigationEntry;
|
|
1223
|
-
else if (getSubcollections(navigationEntry).length > 0) {
|
|
1224
|
-
const newPath = slugOrPath.replace(subpathCombination, "").split("/").slice(2).join("/");
|
|
1225
|
-
if (newPath.length > 0) result = getCollectionBySlugWithin(newPath, getSubcollections(navigationEntry));
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
if (result) break;
|
|
1229
|
-
}
|
|
1230
|
-
return result;
|
|
1231
|
-
}
|
|
1232
|
-
/**
|
|
1233
|
-
* Get the subcollection combinations from a path:
|
|
1234
|
-
* "sites/es/locales" => ["sites/es/locales", "sites"]
|
|
1235
|
-
* @param subpaths
|
|
1236
|
-
*/
|
|
1237
|
-
function getCollectionPathsCombinations(subpaths) {
|
|
1238
|
-
const entries = subpaths.length > 0 && subpaths.length % 2 === 0 ? subpaths.splice(0, subpaths.length - 1) : subpaths;
|
|
1239
|
-
const length = entries.length;
|
|
1240
|
-
const result = [];
|
|
1241
|
-
for (let i = length; i > 0; i = i - 2) result.push(entries.slice(0, i).join("/"));
|
|
1242
|
-
return result;
|
|
1243
|
-
}
|
|
1244
|
-
//#endregion
|
|
1245
|
-
//#region src/util/navigation_from_path.ts
|
|
1246
|
-
function getNavigationEntriesFromPath(props) {
|
|
1247
|
-
const { path, collections = [], currentFullPath } = props;
|
|
1248
|
-
const subpathCombinations = getCollectionPathsCombinations(removeInitialAndTrailingSlashes(path).split("/"));
|
|
1249
|
-
const result = [];
|
|
1250
|
-
for (let i = 0; i < subpathCombinations.length; i++) {
|
|
1251
|
-
const subpathCombination = subpathCombinations[i];
|
|
1252
|
-
const collection = collections && collections.find((entry) => entry.slug === subpathCombination);
|
|
1253
|
-
if (collection) {
|
|
1254
|
-
const collectionPath = currentFullPath && currentFullPath.length > 0 ? currentFullPath + "/" + collection.slug : collection.slug;
|
|
1255
|
-
result.push({
|
|
1256
|
-
type: "collection",
|
|
1257
|
-
id: collection.slug,
|
|
1258
|
-
slug: collectionPath,
|
|
1259
|
-
path: collectionPath,
|
|
1260
|
-
collection
|
|
1261
|
-
});
|
|
1262
|
-
const restOfThePath = removeInitialAndTrailingSlashes(removeInitialAndTrailingSlashes(path).replace(subpathCombination, ""));
|
|
1263
|
-
const nextSegments = restOfThePath.length > 0 ? restOfThePath.split("/") : [];
|
|
1264
|
-
if (nextSegments.length > 0) {
|
|
1265
|
-
const entityId = nextSegments[0];
|
|
1266
|
-
const path = collectionPath + "/" + entityId;
|
|
1267
|
-
result.push({
|
|
1268
|
-
type: "entity",
|
|
1269
|
-
entityId,
|
|
1270
|
-
slug: collectionPath,
|
|
1271
|
-
path,
|
|
1272
|
-
parentCollection: collection
|
|
1273
|
-
});
|
|
1274
|
-
if (nextSegments.length > 1) {
|
|
1275
|
-
const newPath = nextSegments.slice(1).join("/");
|
|
1276
|
-
if (!collection) throw Error("collection not found resolving path: " + collection);
|
|
1277
|
-
const entityViews = collection.entityViews;
|
|
1278
|
-
const customView = entityViews && entityViews.map((entry) => resolveEntityView(entry, props.contextEntityViews)).filter((v) => v != null).find((entry) => entry.key === newPath);
|
|
1279
|
-
const subcollections = getSubcollections(collection);
|
|
1280
|
-
if (customView) result.push({
|
|
1281
|
-
type: "custom_view",
|
|
1282
|
-
slug: collectionPath,
|
|
1283
|
-
entityId,
|
|
1284
|
-
path: path + "/" + customView.key,
|
|
1285
|
-
view: customView
|
|
1286
|
-
});
|
|
1287
|
-
else if (subcollections) result.push(...getNavigationEntriesFromPath({
|
|
1288
|
-
path: newPath,
|
|
1289
|
-
collections: subcollections,
|
|
1290
|
-
currentFullPath: path,
|
|
1291
|
-
contextEntityViews: props.contextEntityViews
|
|
1292
|
-
}));
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
break;
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
return result;
|
|
1299
|
-
}
|
|
1300
|
-
function resolveEntityView(entityView, contextEntityViews) {
|
|
1301
|
-
if (typeof entityView === "string") return contextEntityViews?.find((entry) => entry.key === entityView);
|
|
1302
|
-
else return entityView;
|
|
1303
|
-
}
|
|
1304
|
-
//#endregion
|
|
1305
|
-
//#region src/util/parent_references_from_path.ts
|
|
1306
|
-
function getParentReferencesFromPath(props) {
|
|
1307
|
-
const { path, collections = [], currentFullPath } = props;
|
|
1308
|
-
const subpathCombinations = getCollectionPathsCombinations(removeInitialAndTrailingSlashes(path).split("/"));
|
|
1309
|
-
const result = [];
|
|
1310
|
-
for (let i = 0; i < subpathCombinations.length; i++) {
|
|
1311
|
-
const subpathCombination = subpathCombinations[i];
|
|
1312
|
-
const collection = collections && collections.find((entry) => entry.slug === subpathCombination);
|
|
1313
|
-
if (collection) {
|
|
1314
|
-
const collectionPath = currentFullPath && currentFullPath.length > 0 ? currentFullPath + "/" + collection.slug : collection.slug;
|
|
1315
|
-
const restOfThePath = removeInitialAndTrailingSlashes(removeInitialAndTrailingSlashes(path).replace(subpathCombination, ""));
|
|
1316
|
-
const nextSegments = restOfThePath.length > 0 ? restOfThePath.split("/") : [];
|
|
1317
|
-
if (nextSegments.length > 0) {
|
|
1318
|
-
const entityId = nextSegments[0];
|
|
1319
|
-
const path = collectionPath + "/" + entityId;
|
|
1320
|
-
result.push(new _rebasepro_types.EntityReference({
|
|
1321
|
-
id: entityId,
|
|
1322
|
-
path: collectionPath
|
|
1323
|
-
}));
|
|
1324
|
-
if (nextSegments.length > 1) {
|
|
1325
|
-
const newPath = nextSegments.slice(1).join("/");
|
|
1326
|
-
if (!collection) throw Error("collection not found resolving path: " + collection);
|
|
1327
|
-
if (getSubcollections(collection).length > 0) result.push(...getParentReferencesFromPath({
|
|
1328
|
-
path: newPath,
|
|
1329
|
-
collections: getSubcollections(collection),
|
|
1330
|
-
currentFullPath: path
|
|
1331
|
-
}));
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
break;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
return result;
|
|
1338
|
-
}
|
|
1339
|
-
//#endregion
|
|
1340
|
-
//#region src/util/builders.ts
|
|
1341
|
-
/**
|
|
1342
|
-
* @deprecated Use {@link defineCollection} instead — it infers property
|
|
1343
|
-
* types automatically (autocomplete on `titleProperty`, `sort`,
|
|
1344
|
-
* `propertiesOrder`, callbacks) without manual generics.
|
|
1345
|
-
* `buildCollection` is kept for FireCMS migration compatibility and will
|
|
1346
|
-
* be removed before 1.0.
|
|
1347
|
-
*
|
|
1348
|
-
* @group Builder
|
|
1349
|
-
*/
|
|
1350
|
-
function buildCollection(collection) {
|
|
1351
|
-
return collection;
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Implementation — delegates to the correct overload at the type level.
|
|
1355
|
-
* At runtime this is a plain identity function.
|
|
1356
|
-
*/
|
|
1357
|
-
function defineCollection(collection) {
|
|
1358
|
-
return collection;
|
|
1359
|
-
}
|
|
1360
|
-
/**
|
|
1361
|
-
* @deprecated Use plain typed property objects with {@link defineCollection}
|
|
1362
|
-
* instead — `defineCollection` infers property types automatically, making
|
|
1363
|
-
* this wrapper unnecessary. `buildProperty` is kept for FireCMS migration
|
|
1364
|
-
* compatibility and will be removed before 1.0.
|
|
1365
|
-
*
|
|
1366
|
-
* @group Builder
|
|
1367
|
-
*/
|
|
1368
|
-
function buildProperty(property) {
|
|
1369
|
-
return property;
|
|
1370
|
-
}
|
|
1371
|
-
//#endregion
|
|
1372
|
-
//#region src/util/storage.ts
|
|
1373
|
-
/**
|
|
1374
|
-
* Resolve the {@link StorageSource} to use for a property, given the key
|
|
1375
|
-
* referenced by `StorageConfig.storageSource`.
|
|
1376
|
-
*
|
|
1377
|
-
* Resolution priority:
|
|
1378
|
-
* 1. No `sourceKey` → the default source (backward compatible).
|
|
1379
|
-
* 2. An explicit {@link StorageSourceRegistry} (e.g. `client.storageRegistry`).
|
|
1380
|
-
* 3. A `sources` lookup map (e.g. the `StorageSourcesContext`).
|
|
1381
|
-
* 4. Fall back to the default source.
|
|
1382
|
-
*
|
|
1383
|
-
* Shared by the upload hook, the markdown editor, and the read-only previews
|
|
1384
|
-
* so the resolution logic lives in one place.
|
|
1385
|
-
*
|
|
1386
|
-
* @group Storage
|
|
1387
|
-
*/
|
|
1388
|
-
function resolveStorageSource(params) {
|
|
1389
|
-
const { sourceKey, sources, registry, defaultSource } = params;
|
|
1390
|
-
if (!sourceKey) return defaultSource;
|
|
1391
|
-
if (registry) return registry.getOrDefault(sourceKey);
|
|
1392
|
-
const fromSources = sources?.[sourceKey];
|
|
1393
|
-
if (fromSources) return fromSources;
|
|
1394
|
-
return defaultSource;
|
|
1395
|
-
}
|
|
1396
|
-
async function resolveStorageFilenameString({ input, storage, values, entityId, path, property, file, propertyKey }) {
|
|
1397
|
-
let result;
|
|
1398
|
-
if (typeof input === "function") {
|
|
1399
|
-
result = await input({
|
|
1400
|
-
path,
|
|
1401
|
-
entityId,
|
|
1402
|
-
values,
|
|
1403
|
-
property,
|
|
1404
|
-
file,
|
|
1405
|
-
storage,
|
|
1406
|
-
propertyKey
|
|
1407
|
-
});
|
|
1408
|
-
if (!result) console.warn("Storage callback returned empty result. Using default name value");
|
|
1409
|
-
} else result = replacePlaceholders({
|
|
1410
|
-
file,
|
|
1411
|
-
input,
|
|
1412
|
-
entityId,
|
|
1413
|
-
propertyKey,
|
|
1414
|
-
path
|
|
1415
|
-
});
|
|
1416
|
-
if (!result) result = (0, _rebasepro_utils.randomString)() + "_" + file.name;
|
|
1417
|
-
return result;
|
|
1418
|
-
}
|
|
1419
|
-
function resolveStoragePathString({ input, storage, values, entityId, path, property, file, propertyKey }) {
|
|
1420
|
-
let result;
|
|
1421
|
-
if (typeof input === "function") {
|
|
1422
|
-
result = input({
|
|
1423
|
-
path,
|
|
1424
|
-
entityId,
|
|
1425
|
-
values,
|
|
1426
|
-
property,
|
|
1427
|
-
file,
|
|
1428
|
-
storage,
|
|
1429
|
-
propertyKey
|
|
1430
|
-
});
|
|
1431
|
-
if (!result) console.warn("Storage callback returned empty result. Using default name value");
|
|
1432
|
-
} else result = replacePlaceholders({
|
|
1433
|
-
file,
|
|
1434
|
-
input,
|
|
1435
|
-
entityId,
|
|
1436
|
-
propertyKey,
|
|
1437
|
-
path
|
|
1438
|
-
});
|
|
1439
|
-
if (!result) result = (0, _rebasepro_utils.randomString)() + "_" + file.name;
|
|
1440
|
-
return result;
|
|
1441
|
-
}
|
|
1442
|
-
function replacePlaceholders({ file, input, entityId, propertyKey, path }) {
|
|
1443
|
-
const ext = file.name.split(".").pop();
|
|
1444
|
-
let result = input.replace("{propertyKey}", propertyKey).replace("{rand}", (0, _rebasepro_utils.randomString)()).replace("{file}", file.name).replace("{file.type}", file.type);
|
|
1445
|
-
if (entityId) result = result.replace("{entityId}", String(entityId));
|
|
1446
|
-
if (path) result = result.replace("{path}", path);
|
|
1447
|
-
if (ext) {
|
|
1448
|
-
result = result.replace("{file.ext}", ext);
|
|
1449
|
-
const name = file.name.replace(`.${ext}`, "");
|
|
1450
|
-
result = result.replace("{file.name}", name);
|
|
1451
|
-
}
|
|
1452
|
-
if (!result) result = (0, _rebasepro_utils.randomString)() + "_" + file.name;
|
|
1453
|
-
return result;
|
|
1454
|
-
}
|
|
1455
|
-
//#endregion
|
|
1456
|
-
//#region src/util/callbacks.ts
|
|
1457
|
-
/**
|
|
1458
|
-
* Helper function to recursively check if there are any callbacks in the properties.
|
|
1459
|
-
*/
|
|
1460
|
-
function hasPropertyCallbacks(properties, callbackName) {
|
|
1461
|
-
if (!properties) return false;
|
|
1462
|
-
for (const property of Object.values(properties)) {
|
|
1463
|
-
if (property.callbacks?.[callbackName]) return true;
|
|
1464
|
-
if (property.type === "map" && property.properties) {
|
|
1465
|
-
if (hasPropertyCallbacks(property.properties, callbackName)) return true;
|
|
1466
|
-
} else if (property.type === "array" && property.of) {
|
|
1467
|
-
const ofs = Array.isArray(property.of) ? property.of : [property.of];
|
|
1468
|
-
for (const of of ofs) {
|
|
1469
|
-
if (of.callbacks?.[callbackName]) return true;
|
|
1470
|
-
if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
return false;
|
|
1475
|
-
}
|
|
1476
|
-
/**
|
|
1477
|
-
* Recursively process properties to apply field-level hooks.
|
|
1478
|
-
*/
|
|
1479
|
-
async function processProperties(properties, values, previousValues, propsContext, callbackName) {
|
|
1480
|
-
if (!values || typeof values !== "object") return values;
|
|
1481
|
-
const result = { ...values };
|
|
1482
|
-
for (const [key, property] of Object.entries(properties)) {
|
|
1483
|
-
if (result[key] === void 0) continue;
|
|
1484
|
-
let currentValue = result[key];
|
|
1485
|
-
const previousValue = previousValues?.[key];
|
|
1486
|
-
if (property.type === "array" && Array.isArray(currentValue)) {
|
|
1487
|
-
if (property.of && !Array.isArray(property.of)) currentValue = await Promise.all(currentValue.map(async (item, index) => {
|
|
1488
|
-
const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
|
|
1489
|
-
return (await processProperties({ "_tmp": property.of }, { "_tmp": item }, { "_tmp": prevItem }, propsContext, callbackName))["_tmp"];
|
|
1490
|
-
}));
|
|
1491
|
-
} else if (property.type === "map" && property.properties && typeof currentValue === "object") currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
|
|
1492
|
-
if (property.callbacks?.[callbackName]) {
|
|
1493
|
-
const cbRes = await Promise.resolve(property.callbacks[callbackName]({
|
|
1494
|
-
...propsContext,
|
|
1495
|
-
value: currentValue,
|
|
1496
|
-
previousValue
|
|
1497
|
-
}));
|
|
1498
|
-
if (cbRes !== void 0) currentValue = cbRes;
|
|
1499
|
-
}
|
|
1500
|
-
result[key] = currentValue;
|
|
1501
|
-
}
|
|
1502
|
-
return result;
|
|
1503
|
-
}
|
|
1504
|
-
/**
|
|
1505
|
-
* Helper function to extract field-level PropertyCallbacks from a properties schema
|
|
1506
|
-
* and wrap them into an CollectionCallbacks object recursively.
|
|
1507
|
-
*/
|
|
1508
|
-
var buildPropertyCallbacks = (properties) => {
|
|
1509
|
-
if (!properties) return void 0;
|
|
1510
|
-
const propertyCallbacks = {};
|
|
1511
|
-
if (hasPropertyCallbacks(properties, "afterRead")) propertyCallbacks.afterRead = async (props) => {
|
|
1512
|
-
const row = props.row;
|
|
1513
|
-
const processedValues = await processProperties(properties, row, row, props, "afterRead");
|
|
1514
|
-
return {
|
|
1515
|
-
...props.row,
|
|
1516
|
-
...processedValues
|
|
1517
|
-
};
|
|
1518
|
-
};
|
|
1519
|
-
if (hasPropertyCallbacks(properties, "beforeSave")) propertyCallbacks.beforeSave = async (props) => {
|
|
1520
|
-
return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
|
|
1521
|
-
};
|
|
1522
|
-
return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
|
|
1523
|
-
};
|
|
1524
|
-
//#endregion
|
|
1525
|
-
//#region src/util/conditions.ts
|
|
1526
|
-
/**
|
|
1527
|
-
* Access a nested property from an object via dot notation.
|
|
1528
|
-
*/
|
|
1529
|
-
function getIn(obj, path) {
|
|
1530
|
-
if (!obj || !path) return void 0;
|
|
1531
|
-
return path.split(".").reduce((acc, part) => acc && acc[part], obj);
|
|
1532
|
-
}
|
|
1533
|
-
var operationsRegistered = false;
|
|
1534
|
-
/**
|
|
1535
|
-
* Register custom JSON Logic operations for Rebase.
|
|
1536
|
-
* Call this once at app initialization.
|
|
1537
|
-
*/
|
|
1538
|
-
function registerConditionOperations() {
|
|
1539
|
-
if (operationsRegistered) return;
|
|
1540
|
-
json_logic_js.default.add_operation("hasRole", function(roleId) {
|
|
1541
|
-
return this?.user?.roles?.includes(roleId) ?? false;
|
|
1542
|
-
});
|
|
1543
|
-
json_logic_js.default.add_operation("hasAnyRole", function(roleIds) {
|
|
1544
|
-
if (!this?.user?.roles || !Array.isArray(roleIds)) return false;
|
|
1545
|
-
return roleIds.some((role) => this.user.roles.includes(role));
|
|
1546
|
-
});
|
|
1547
|
-
json_logic_js.default.add_operation("isToday", (timestamp) => {
|
|
1548
|
-
if (!timestamp) return false;
|
|
1549
|
-
const date = new Date(timestamp);
|
|
1550
|
-
const today = /* @__PURE__ */ new Date();
|
|
1551
|
-
return date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate();
|
|
1552
|
-
});
|
|
1553
|
-
json_logic_js.default.add_operation("isPast", (timestamp) => {
|
|
1554
|
-
if (!timestamp) return false;
|
|
1555
|
-
return timestamp < Date.now();
|
|
1556
|
-
});
|
|
1557
|
-
json_logic_js.default.add_operation("isFuture", (timestamp) => {
|
|
1558
|
-
if (!timestamp) return false;
|
|
1559
|
-
return timestamp > Date.now();
|
|
1560
|
-
});
|
|
1561
|
-
operationsRegistered = true;
|
|
1562
|
-
}
|
|
1563
|
-
/**
|
|
1564
|
-
* Evaluate a JSON Logic rule against the given context.
|
|
1565
|
-
*/
|
|
1566
|
-
function evaluateCondition(rule, context) {
|
|
1567
|
-
registerConditionOperations();
|
|
1568
|
-
return json_logic_js.default.apply(rule, context);
|
|
1569
|
-
}
|
|
1570
|
-
/**
|
|
1571
|
-
* Convert a value to a format suitable for JSON Logic evaluation.
|
|
1572
|
-
* Specifically handles Date objects by converting them to Unix timestamps.
|
|
1573
|
-
*/
|
|
1574
|
-
function serializeValueForConditions(value) {
|
|
1575
|
-
if (value === null || value === void 0) return value;
|
|
1576
|
-
if (value instanceof Date) return value.getTime();
|
|
1577
|
-
if (typeof value?.toMillis === "function") return value.toMillis();
|
|
1578
|
-
if (typeof value?.toDate === "function") return value.toDate().getTime();
|
|
1579
|
-
if (Array.isArray(value)) return value.map(serializeValueForConditions);
|
|
1580
|
-
if (typeof value === "object") {
|
|
1581
|
-
const result = {};
|
|
1582
|
-
for (const key of Object.keys(value)) result[key] = serializeValueForConditions(value[key]);
|
|
1583
|
-
return result;
|
|
1584
|
-
}
|
|
1585
|
-
return value;
|
|
1586
|
-
}
|
|
1587
|
-
/**
|
|
1588
|
-
* Build a ConditionContext from the current property resolution context.
|
|
1589
|
-
*/
|
|
1590
|
-
function buildConditionContext(params) {
|
|
1591
|
-
const { propertyKey, values, previousValues, path, entityId, index, authController } = params;
|
|
1592
|
-
const user = authController.user;
|
|
1593
|
-
const serializedValues = serializeValueForConditions(values ?? {});
|
|
1594
|
-
return {
|
|
1595
|
-
values: serializedValues,
|
|
1596
|
-
previousValues: serializeValueForConditions(previousValues ?? values ?? {}),
|
|
1597
|
-
propertyValue: propertyKey ? getIn(serializedValues, propertyKey) : void 0,
|
|
1598
|
-
path,
|
|
1599
|
-
entityId,
|
|
1600
|
-
isNew: !entityId,
|
|
1601
|
-
index,
|
|
1602
|
-
user: {
|
|
1603
|
-
uid: user?.uid ?? "",
|
|
1604
|
-
email: user?.email ?? null,
|
|
1605
|
-
displayName: user?.displayName ?? null,
|
|
1606
|
-
photoURL: user?.photoURL ?? null,
|
|
1607
|
-
roles: (user?.roles ?? []).map((r) => typeof r === "string" ? r : r.id)
|
|
1608
|
-
},
|
|
1609
|
-
now: Date.now()
|
|
1610
|
-
};
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
|
|
1614
|
-
*/
|
|
1615
|
-
function applyPropertyConditions(property, context) {
|
|
1616
|
-
const { conditions } = property;
|
|
1617
|
-
if (!conditions) return property;
|
|
1618
|
-
const result = { ...property };
|
|
1619
|
-
if (conditions.disabled) {
|
|
1620
|
-
if (evaluateCondition(conditions.disabled, context)) {
|
|
1621
|
-
result.ui = result.ui || {};
|
|
1622
|
-
result.ui.disabled = {
|
|
1623
|
-
clearOnDisabled: conditions.clearOnDisabled ?? false,
|
|
1624
|
-
disabledMessage: conditions.disabledMessage,
|
|
1625
|
-
hidden: false
|
|
1626
|
-
};
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
if (conditions.hidden) {
|
|
1630
|
-
if (evaluateCondition(conditions.hidden, context)) {
|
|
1631
|
-
result.ui = result.ui || {};
|
|
1632
|
-
result.ui.disabled = {
|
|
1633
|
-
...typeof result.ui?.disabled === "object" ? result.ui.disabled : {},
|
|
1634
|
-
hidden: true,
|
|
1635
|
-
clearOnDisabled: conditions.clearOnDisabled ?? false
|
|
1636
|
-
};
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
if (conditions.readOnly) {
|
|
1640
|
-
if (evaluateCondition(conditions.readOnly, context)) {
|
|
1641
|
-
result.ui = result.ui || {};
|
|
1642
|
-
result.ui.readOnly = true;
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
if (conditions.required !== void 0) {
|
|
1646
|
-
const isRequired = evaluateCondition(conditions.required, context);
|
|
1647
|
-
result.validation = {
|
|
1648
|
-
...result.validation,
|
|
1649
|
-
required: isRequired,
|
|
1650
|
-
requiredMessage: conditions.requiredMessage
|
|
1651
|
-
};
|
|
1652
|
-
}
|
|
1653
|
-
if (context.isNew && conditions.defaultValue !== void 0) result.defaultValue = evaluateCondition(conditions.defaultValue, context);
|
|
1654
|
-
if ("enum" in result && result.enum && (conditions.enumConditions || conditions.allowedEnumValues || conditions.excludedEnumValues)) result.enum = applyEnumConditions(result.enum, conditions, context);
|
|
1655
|
-
if (result.type === "reference") {
|
|
1656
|
-
if (conditions.referencePath) result.path = evaluateCondition(conditions.referencePath, context);
|
|
1657
|
-
if (conditions.referenceFilter) result.fixedFilter = evaluateCondition(conditions.referenceFilter, context);
|
|
1658
|
-
}
|
|
1659
|
-
if (result.type === "array") {
|
|
1660
|
-
if (conditions.canAddElements !== void 0) result.canAddElements = evaluateCondition(conditions.canAddElements, context);
|
|
1661
|
-
if (conditions.sortable !== void 0) result.sortable = evaluateCondition(conditions.sortable, context);
|
|
1662
|
-
}
|
|
1663
|
-
return result;
|
|
1664
|
-
}
|
|
1665
|
-
/**
|
|
1666
|
-
* Convert an object with numeric keys back to an array.
|
|
1667
|
-
* Firestore stores arrays as {"0": "a", "1": "b"} to avoid nested arrays.
|
|
1668
|
-
*/
|
|
1669
|
-
function objectToArray(obj) {
|
|
1670
|
-
if (Array.isArray(obj)) return obj.map(String);
|
|
1671
|
-
if (obj && typeof obj === "object") {
|
|
1672
|
-
const keys = Object.keys(obj);
|
|
1673
|
-
if (keys.length > 0 && keys.every((k) => !isNaN(Number(k)))) return keys.sort((a, b) => Number(a) - Number(b)).map((k) => obj[k]).filter((v) => typeof v === "string" || typeof v === "number").map(String);
|
|
1674
|
-
}
|
|
1675
|
-
return [];
|
|
1676
|
-
}
|
|
1677
|
-
/**
|
|
1678
|
-
* Apply enum-specific conditions to filter and modify enum values.
|
|
1679
|
-
*/
|
|
1680
|
-
function applyEnumConditions(enumValues, conditions, context) {
|
|
1681
|
-
let result = [...enumValues];
|
|
1682
|
-
if (conditions.allowedEnumValues) {
|
|
1683
|
-
const allowedArray = objectToArray(evaluateCondition(conditions.allowedEnumValues, context));
|
|
1684
|
-
if (allowedArray.length > 0) result = result.filter((ev) => allowedArray.includes(String(ev.id)));
|
|
1685
|
-
}
|
|
1686
|
-
if (conditions.excludedEnumValues) {
|
|
1687
|
-
const excludedArray = objectToArray(evaluateCondition(conditions.excludedEnumValues, context));
|
|
1688
|
-
if (excludedArray.length > 0) result = result.filter((ev) => !excludedArray.includes(String(ev.id)));
|
|
1689
|
-
}
|
|
1690
|
-
if (conditions.enumConditions) result = result.map((ev) => {
|
|
1691
|
-
const evConditions = conditions.enumConditions?.[ev.id];
|
|
1692
|
-
if (!evConditions) return ev;
|
|
1693
|
-
if (evConditions.hidden && evaluateCondition(evConditions.hidden, context)) return null;
|
|
1694
|
-
if (evConditions.disabled && evaluateCondition(evConditions.disabled, context)) return {
|
|
1695
|
-
...ev,
|
|
1696
|
-
disabled: true
|
|
1697
|
-
};
|
|
1698
|
-
return ev;
|
|
1699
|
-
}).filter((ev) => ev !== null);
|
|
1700
|
-
return result;
|
|
1701
|
-
}
|
|
1702
|
-
//#endregion
|
|
1703
|
-
//#region src/util/filter-operator-resolution.ts
|
|
1704
|
-
/**
|
|
1705
|
-
* Default operators offered per property type, before engine capabilities and
|
|
1706
|
-
* per-property narrowing are applied. These mirror what the built-in filter
|
|
1707
|
-
* fields can render.
|
|
1708
|
-
*/
|
|
1709
|
-
var COMPARISON_OPS = [
|
|
1710
|
-
"==",
|
|
1711
|
-
"!=",
|
|
1712
|
-
">",
|
|
1713
|
-
">=",
|
|
1714
|
-
"<",
|
|
1715
|
-
"<="
|
|
1716
|
-
];
|
|
1717
|
-
var NULL_CHECK_OPS = ["is-null", "is-not-null"];
|
|
1718
|
-
var MEMBERSHIP_OPS = ["in", "not-in"];
|
|
1719
|
-
var PATTERN_OPS = [
|
|
1720
|
-
"like",
|
|
1721
|
-
"ilike",
|
|
1722
|
-
"not-like",
|
|
1723
|
-
"not-ilike"
|
|
1724
|
-
];
|
|
1725
|
-
var DEFAULT_OPS_BY_TYPE = {
|
|
1726
|
-
string: [
|
|
1727
|
-
...COMPARISON_OPS,
|
|
1728
|
-
...MEMBERSHIP_OPS,
|
|
1729
|
-
...PATTERN_OPS,
|
|
1730
|
-
...NULL_CHECK_OPS
|
|
1731
|
-
],
|
|
1732
|
-
number: [
|
|
1733
|
-
...COMPARISON_OPS,
|
|
1734
|
-
...MEMBERSHIP_OPS,
|
|
1735
|
-
...NULL_CHECK_OPS
|
|
1736
|
-
],
|
|
1737
|
-
date: [...COMPARISON_OPS, ...NULL_CHECK_OPS],
|
|
1738
|
-
boolean: [
|
|
1739
|
-
"==",
|
|
1740
|
-
"!=",
|
|
1741
|
-
...NULL_CHECK_OPS
|
|
1742
|
-
],
|
|
1743
|
-
reference: [
|
|
1744
|
-
"==",
|
|
1745
|
-
"!=",
|
|
1746
|
-
...MEMBERSHIP_OPS,
|
|
1747
|
-
...NULL_CHECK_OPS
|
|
1748
|
-
],
|
|
1749
|
-
relation: [
|
|
1750
|
-
"==",
|
|
1751
|
-
"!=",
|
|
1752
|
-
...MEMBERSHIP_OPS,
|
|
1753
|
-
...NULL_CHECK_OPS
|
|
1754
|
-
]
|
|
1755
|
-
};
|
|
1756
|
-
/** Operators offered when the property is an *array of* a filterable type. */
|
|
1757
|
-
var ARRAY_OPS = ["array-contains", "array-contains-any"];
|
|
1758
|
-
/**
|
|
1759
|
-
* Resolve which filter operators the UI should offer for a property.
|
|
1760
|
-
*
|
|
1761
|
-
* The result is the **intersection** of three sets:
|
|
1762
|
-
* 1. what the engine can execute — {@link DataSourceCapabilities.filterOperators}
|
|
1763
|
-
* (e.g. Firestore cannot run the LIKE family);
|
|
1764
|
-
* 2. what makes sense for the property type (e.g. no `>` on booleans);
|
|
1765
|
-
* 3. the developer's optional narrowing — `property.ui.filterOperators`.
|
|
1766
|
-
*
|
|
1767
|
-
* Returns an empty array when the property is not filterable (either by
|
|
1768
|
-
* type, or because the developer disabled it with `filterOperators: []`).
|
|
1769
|
-
*
|
|
1770
|
-
* @group Models
|
|
1771
|
-
*/
|
|
1772
|
-
function resolveFilterOperators({ property, isArray, engine }) {
|
|
1773
|
-
const typeDefaults = isArray ? ARRAY_OPS : DEFAULT_OPS_BY_TYPE[property.type] ?? [];
|
|
1774
|
-
if (typeDefaults.length === 0) return [];
|
|
1775
|
-
const engineOps = new Set((0, _rebasepro_types.getDataSourceCapabilities)(engine).filterOperators ?? _rebasepro_types.ALL_WHERE_FILTER_OPS);
|
|
1776
|
-
const narrowing = property.ui?.filterOperators;
|
|
1777
|
-
const narrowingSet = narrowing !== void 0 ? new Set(narrowing) : void 0;
|
|
1778
|
-
return typeDefaults.filter((op) => engineOps.has(op) && (narrowingSet === void 0 || narrowingSet.has(op)));
|
|
1779
|
-
}
|
|
1780
|
-
//#endregion
|
|
1781
|
-
//#region src/data/resolveDataSource.ts
|
|
1782
|
-
/**
|
|
1783
|
-
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
1784
|
-
* Later entries win on key collision.
|
|
1785
|
-
*/
|
|
1786
|
-
function createDataSourceRegistry(definitions) {
|
|
1787
|
-
const registry = {};
|
|
1788
|
-
for (const def of definitions ?? []) registry[def.key] = def;
|
|
1789
|
-
return registry;
|
|
1790
|
-
}
|
|
1791
|
-
/**
|
|
1792
|
-
* Resolve the effective data source for a collection — the single source of
|
|
1793
|
-
* truth shared by the frontend router, the backend driver registry, and the
|
|
1794
|
-
* editor's capability lookups.
|
|
1795
|
-
*
|
|
1796
|
-
* Resolution order:
|
|
1797
|
-
* 1. The routing **key** is `collection.dataSource`, else
|
|
1798
|
-
* {@link DEFAULT_DATA_SOURCE_KEY}.
|
|
1799
|
-
* 2. If a definition is registered for that key, it provides `engine`,
|
|
1800
|
-
* `transport`, and `databaseId`.
|
|
1801
|
-
* 3. Otherwise values are synthesized: `engine` from `collection.engine`
|
|
1802
|
-
* (or the key, or `"postgres"`), `transport` defaults to `"server"`,
|
|
1803
|
-
* and `databaseId` from the collection.
|
|
1804
|
-
*
|
|
1805
|
-
* `capabilities` are always derived from the resolved `engine`, so two
|
|
1806
|
-
* data sources sharing an engine share capabilities.
|
|
1807
|
-
*
|
|
1808
|
-
* @param collection the collection (or any object carrying the routing fields)
|
|
1809
|
-
* @param registry optional registry of declared data sources
|
|
1810
|
-
*/
|
|
1811
|
-
function resolveDataSource(collection, registry) {
|
|
1812
|
-
const key = collection?.dataSource ?? _rebasepro_types.DEFAULT_DATA_SOURCE_KEY;
|
|
1813
|
-
const def = registry?.[key];
|
|
1814
|
-
const engine = def?.engine ?? collection?.engine ?? (key !== _rebasepro_types.DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
|
|
1815
|
-
return {
|
|
1816
|
-
key,
|
|
1817
|
-
engine,
|
|
1818
|
-
transport: def?.transport ?? "server",
|
|
1819
|
-
databaseId: collection?.databaseId ?? def?.databaseId,
|
|
1820
|
-
capabilities: (0, _rebasepro_types.getDataSourceCapabilities)(engine)
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
//#endregion
|
|
1824
|
-
//#region src/collections/CollectionRegistry.ts
|
|
1825
|
-
var CollectionRegistry = class {
|
|
1826
|
-
/**
|
|
1827
|
-
* Declared data sources, used during normalization to resolve each
|
|
1828
|
-
* collection's engine (so `dataSource`-only collections get the right
|
|
1829
|
-
* capabilities). Empty by default.
|
|
1830
|
-
*/
|
|
1831
|
-
dataSources = {};
|
|
1832
|
-
/**
|
|
1833
|
-
* Global lifecycle callbacks applied to every collection.
|
|
1834
|
-
* Runs on all data paths (REST, WebSocket, `rebase.data`).
|
|
1835
|
-
* Execution order: global → collection → property callbacks.
|
|
1836
|
-
*/
|
|
1837
|
-
_globalCallbacks;
|
|
1838
|
-
/**
|
|
1839
|
-
* Set global lifecycle callbacks that apply to every collection.
|
|
1840
|
-
* Typically called once during backend initialization.
|
|
1841
|
-
*/
|
|
1842
|
-
setGlobalCallbacks(callbacks) {
|
|
1843
|
-
this._globalCallbacks = callbacks;
|
|
1844
|
-
}
|
|
1845
|
-
/**
|
|
1846
|
-
* Get the currently registered global callbacks, if any.
|
|
1847
|
-
*/
|
|
1848
|
-
getGlobalCallbacks() {
|
|
1849
|
-
return this._globalCallbacks;
|
|
1850
|
-
}
|
|
1851
|
-
collectionsByTableName = /* @__PURE__ */ new Map();
|
|
1852
|
-
collectionsBySlug = /* @__PURE__ */ new Map();
|
|
1853
|
-
rootCollections = [];
|
|
1854
|
-
cachedCollectionsList = null;
|
|
1855
|
-
rawCollectionsByTableName = /* @__PURE__ */ new Map();
|
|
1856
|
-
rawCollectionsBySlug = /* @__PURE__ */ new Map();
|
|
1857
|
-
rawRootCollections = [];
|
|
1858
|
-
cachedRawCollectionsList = null;
|
|
1859
|
-
lastRawInputEntity = null;
|
|
1860
|
-
constructor(collections, dataSources) {
|
|
1861
|
-
if (dataSources) this.dataSources = dataSources;
|
|
1862
|
-
if (collections) this.registerMultiple(collections);
|
|
1863
|
-
}
|
|
1864
|
-
/**
|
|
1865
|
-
* Provide the declared data sources used to resolve each collection's
|
|
1866
|
-
* engine during normalization. Set this before registering collections.
|
|
1867
|
-
* Returns true if the registry changed (callers may re-register).
|
|
1868
|
-
*/
|
|
1869
|
-
setDataSources(dataSources) {
|
|
1870
|
-
if ((0, fast_equals.deepEqual)(this.dataSources, dataSources)) return false;
|
|
1871
|
-
this.dataSources = dataSources ?? {};
|
|
1872
|
-
return true;
|
|
1873
|
-
}
|
|
1874
|
-
reset() {
|
|
1875
|
-
this.collectionsByTableName.clear();
|
|
1876
|
-
this.collectionsBySlug.clear();
|
|
1877
|
-
this.rootCollections = [];
|
|
1878
|
-
this.cachedCollectionsList = null;
|
|
1879
|
-
this.rawCollectionsByTableName.clear();
|
|
1880
|
-
this.rawCollectionsBySlug.clear();
|
|
1881
|
-
this.rawRootCollections = [];
|
|
1882
|
-
this.cachedRawCollectionsList = null;
|
|
1883
|
-
}
|
|
1884
|
-
/**
|
|
1885
|
-
* Registers a collection and its subcollections recursively.
|
|
1886
|
-
* Returns true if the collections have changed, false otherwise.
|
|
1887
|
-
*
|
|
1888
|
-
* Idempotent: compares the raw input (before normalization) against a stored
|
|
1889
|
-
* entity. Only re-normalizes and re-registers when the raw input actually changed.
|
|
1890
|
-
* @param collections
|
|
1891
|
-
*/
|
|
1892
|
-
registerMultiple(collections) {
|
|
1893
|
-
const rawEntity = collections.map((c) => (0, _rebasepro_utils.removeFunctions)(c));
|
|
1894
|
-
if (this.lastRawInputEntity && (0, fast_equals.deepEqual)(this.lastRawInputEntity, rawEntity)) return false;
|
|
1895
|
-
this.reset();
|
|
1896
|
-
collections.forEach((c) => {
|
|
1897
|
-
if (c.slug) this.collectionsBySlug.set(c.slug, c);
|
|
1898
|
-
this.collectionsByTableName.set(getTableName(c), c);
|
|
1899
|
-
});
|
|
1900
|
-
const normalizedCollections = collections.map((c) => this.normalizeCollection({ ...c }));
|
|
1901
|
-
normalizedCollections.forEach((c, index) => {
|
|
1902
|
-
const raw = (0, _rebasepro_utils.deepClone)(collections[index]);
|
|
1903
|
-
this.rootCollections.push(c);
|
|
1904
|
-
this.rawRootCollections.push(raw);
|
|
1905
|
-
const normalized = this.normalizeCollection(c);
|
|
1906
|
-
this.collectionsByTableName.set(getTableName(normalized), normalized);
|
|
1907
|
-
this.rawCollectionsByTableName.set(getTableName(raw), raw);
|
|
1908
|
-
if (normalized.slug) this.collectionsBySlug.set(normalized.slug, normalized);
|
|
1909
|
-
if (raw.slug) this.rawCollectionsBySlug.set(raw.slug, raw);
|
|
1910
|
-
});
|
|
1911
|
-
normalizedCollections.forEach((c) => {
|
|
1912
|
-
const subcollections = getSubcollections(c);
|
|
1913
|
-
if (subcollections && subcollections.length > 0) subcollections.forEach((subCollection) => {
|
|
1914
|
-
if (!subCollection) return;
|
|
1915
|
-
this._registerRecursively(this.normalizeCollection({ ...subCollection }), (0, _rebasepro_utils.deepClone)(subCollection));
|
|
1916
|
-
});
|
|
1917
|
-
});
|
|
1918
|
-
this.lastRawInputEntity = rawEntity;
|
|
1919
|
-
return true;
|
|
1920
|
-
}
|
|
1921
|
-
register(collection, rawCollection) {
|
|
1922
|
-
const raw = rawCollection ? (0, _rebasepro_utils.deepClone)(rawCollection) : (0, _rebasepro_utils.deepClone)(collection);
|
|
1923
|
-
this.rootCollections.push(collection);
|
|
1924
|
-
this.rawRootCollections.push(raw);
|
|
1925
|
-
this._registerRecursively(collection, raw);
|
|
1926
|
-
}
|
|
1927
|
-
_registerRecursively(collection, rawCollection) {
|
|
1928
|
-
if (this.collectionsByTableName.has(getTableName(collection))) return;
|
|
1929
|
-
const normalizedCollection = this.normalizeCollection(collection);
|
|
1930
|
-
this.collectionsByTableName.set(getTableName(normalizedCollection), normalizedCollection);
|
|
1931
|
-
this.rawCollectionsByTableName.set(getTableName(rawCollection), rawCollection);
|
|
1932
|
-
if (normalizedCollection.slug) this.collectionsBySlug.set(normalizedCollection.slug, normalizedCollection);
|
|
1933
|
-
if (rawCollection.slug) this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);
|
|
1934
|
-
const subcollections = getSubcollections(normalizedCollection);
|
|
1935
|
-
if (subcollections && subcollections.length > 0) subcollections.forEach((subCollection) => {
|
|
1936
|
-
if (!subCollection) return;
|
|
1937
|
-
this._registerRecursively(this.normalizeCollection({ ...subCollection }), (0, _rebasepro_utils.deepClone)(subCollection));
|
|
1938
|
-
});
|
|
1939
|
-
}
|
|
1940
|
-
normalizeCollection(collection) {
|
|
1941
|
-
const result = { ...collection };
|
|
1942
|
-
{
|
|
1943
|
-
const resolved = resolveDataSource(result, this.dataSources);
|
|
1944
|
-
if (!result.dataSource) result.dataSource = resolved.key;
|
|
1945
|
-
if (!result.engine) result.engine = resolved.engine;
|
|
1946
|
-
}
|
|
1947
|
-
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
1948
|
-
const relResult = result;
|
|
1949
|
-
const manualRelations = (0, _rebasepro_types.getDataSourceCapabilities)(result.engine).supportsRelations ? relResult.relations ?? [] : [];
|
|
1950
|
-
const mergedRelationsRaw = [...extractedRelations];
|
|
1951
|
-
for (const manual of manualRelations) {
|
|
1952
|
-
const name = manual.relationName;
|
|
1953
|
-
if (!name) mergedRelationsRaw.push(manual);
|
|
1954
|
-
else {
|
|
1955
|
-
const existingIndex = mergedRelationsRaw.findIndex((r) => r.relationName === name);
|
|
1956
|
-
if (existingIndex === -1) mergedRelationsRaw.push(manual);
|
|
1957
|
-
else mergedRelationsRaw[existingIndex] = {
|
|
1958
|
-
...manual,
|
|
1959
|
-
...mergedRelationsRaw[existingIndex]
|
|
1960
|
-
};
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
let mergedRelations = mergedRelationsRaw;
|
|
1964
|
-
if ((0, _rebasepro_types.getDataSourceCapabilities)(result.engine).supportsRelations) {
|
|
1965
|
-
mergedRelations = mergedRelationsRaw.map((r) => {
|
|
1966
|
-
try {
|
|
1967
|
-
return sanitizeRelation(r, result, (slug) => this.get(slug));
|
|
1968
|
-
} catch {
|
|
1969
|
-
return r;
|
|
1970
|
-
}
|
|
1971
|
-
});
|
|
1972
|
-
relResult.relations = mergedRelations;
|
|
1973
|
-
}
|
|
1974
|
-
result.properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
1975
|
-
if (!result.childCollections) {
|
|
1976
|
-
const capabilities = (0, _rebasepro_types.getDataSourceCapabilities)(result.engine);
|
|
1977
|
-
const declaredSubcollections = (0, _rebasepro_types.getDeclaredSubcollections)(result);
|
|
1978
|
-
if (capabilities.supportsSubcollections && declaredSubcollections) result.childCollections = declaredSubcollections;
|
|
1979
|
-
else if (capabilities.supportsRelations && relResult.relations) {
|
|
1980
|
-
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
1981
|
-
if (manyRelations.length > 0) result.childCollections = () => manyRelations.map((r) => {
|
|
1982
|
-
const target = r.target();
|
|
1983
|
-
return r.overrides ? (0, _rebasepro_utils.mergeDeep)(target, r.overrides) : target;
|
|
1984
|
-
});
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
return result;
|
|
1988
|
-
}
|
|
1989
|
-
/**
|
|
1990
|
-
* Extract Relation[] from properties that have inline relation config (i.e. `target` is set).
|
|
1991
|
-
* This allows developers to define relations directly on properties without a separate
|
|
1992
|
-
* `relations[]` entry on the collection.
|
|
1993
|
-
*/
|
|
1994
|
-
extractRelationsFromProperties(properties) {
|
|
1995
|
-
const relations = [];
|
|
1996
|
-
for (const [key, property] of Object.entries(properties)) if (property.type === "relation") {
|
|
1997
|
-
const relProp = property;
|
|
1998
|
-
const target = relProp.target ?? relProp.relation?.target;
|
|
1999
|
-
if (target) {
|
|
2000
|
-
const relationName = relProp.relationName ?? relProp.relation?.relationName ?? key;
|
|
2001
|
-
relations.push({
|
|
2002
|
-
relationName,
|
|
2003
|
-
target,
|
|
2004
|
-
cardinality: relProp.cardinality ?? relProp.relation?.cardinality ?? "one",
|
|
2005
|
-
direction: relProp.direction ?? relProp.relation?.direction ?? "owning",
|
|
2006
|
-
inverseRelationName: relProp.inverseRelationName ?? relProp.relation?.inverseRelationName,
|
|
2007
|
-
localKey: relProp.localKey ?? relProp.relation?.localKey,
|
|
2008
|
-
foreignKeyOnTarget: relProp.foreignKeyOnTarget ?? relProp.relation?.foreignKeyOnTarget,
|
|
2009
|
-
through: relProp.through ?? relProp.relation?.through,
|
|
2010
|
-
joinPath: relProp.joinPath ?? relProp.relation?.joinPath,
|
|
2011
|
-
onUpdate: relProp.onUpdate ?? relProp.relation?.onUpdate,
|
|
2012
|
-
onDelete: relProp.onDelete ?? relProp.relation?.onDelete,
|
|
2013
|
-
overrides: relProp.overrides ?? relProp.relation?.overrides
|
|
2014
|
-
});
|
|
2015
|
-
}
|
|
2016
|
-
} else if (property.type === "map" && property.properties) relations.push(...this.extractRelationsFromProperties(property.properties));
|
|
2017
|
-
return relations;
|
|
2018
|
-
}
|
|
2019
|
-
normalizeProperties(properties, relations) {
|
|
2020
|
-
const newProperties = {};
|
|
2021
|
-
for (const key in properties) newProperties[key] = this.normalizeProperty(key, properties[key], relations);
|
|
2022
|
-
return newProperties;
|
|
2023
|
-
}
|
|
2024
|
-
normalizeProperty(key, property, relations) {
|
|
2025
|
-
const newProperty = { ...property };
|
|
2026
|
-
if (newProperty.type === "map" && newProperty.properties) newProperty.properties = this.normalizeProperties(newProperty.properties, relations);
|
|
2027
|
-
else if (newProperty.type === "array") {
|
|
2028
|
-
const arrayProp = newProperty;
|
|
2029
|
-
if (arrayProp.of) if (Array.isArray(arrayProp.of)) arrayProp.of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, relations));
|
|
2030
|
-
else arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, relations);
|
|
2031
|
-
else if (arrayProp.oneOf && arrayProp.oneOf.properties) arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, relations);
|
|
2032
|
-
} else if ((newProperty.type === "string" || newProperty.type === "number") && newProperty.enum) {
|
|
2033
|
-
const stringOrNumberProperty = newProperty;
|
|
2034
|
-
if (typeof stringOrNumberProperty.enum === "object" && !Array.isArray(stringOrNumberProperty.enum)) stringOrNumberProperty.enum = enumToObjectEntries(stringOrNumberProperty.enum)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? [];
|
|
2035
|
-
} else if (newProperty.type === "relation") {
|
|
2036
|
-
const relationProperty = newProperty;
|
|
2037
|
-
const name = relationProperty.relationName || key;
|
|
2038
|
-
const relation = relations.find((r) => r.relationName === name);
|
|
2039
|
-
if (relation) relationProperty.relation = relation;
|
|
2040
|
-
else console.warn(`Could not find relation for property '${key}' with relationName: ${name}`);
|
|
2041
|
-
}
|
|
2042
|
-
return newProperty;
|
|
2043
|
-
}
|
|
2044
|
-
get(path) {
|
|
2045
|
-
const bySlug = this.collectionsBySlug.get(path);
|
|
2046
|
-
if (bySlug) return bySlug;
|
|
2047
|
-
if (path.includes("-")) {
|
|
2048
|
-
const normalized = path.replace(/-/g, "_");
|
|
2049
|
-
const byNormalized = this.collectionsBySlug.get(normalized);
|
|
2050
|
-
if (byNormalized) return byNormalized;
|
|
2051
|
-
}
|
|
2052
|
-
return this.collectionsByTableName.get(path);
|
|
2053
|
-
}
|
|
2054
|
-
/**
|
|
2055
|
-
* Gets the pristine, un-normalized collection exactly as it was provided.
|
|
2056
|
-
* Useful for the AST editor so it doesn't accidentally serialize injected metadata back to disk.
|
|
2057
|
-
*/
|
|
2058
|
-
getRaw(path) {
|
|
2059
|
-
const bySlug = this.rawCollectionsBySlug.get(path);
|
|
2060
|
-
if (bySlug) return bySlug;
|
|
2061
|
-
if (path.includes("-")) {
|
|
2062
|
-
const normalized = path.replace(/-/g, "_");
|
|
2063
|
-
const byNormalized = this.rawCollectionsBySlug.get(normalized);
|
|
2064
|
-
if (byNormalized) return byNormalized;
|
|
2065
|
-
}
|
|
2066
|
-
return this.rawCollectionsByTableName.get(path);
|
|
2067
|
-
}
|
|
2068
|
-
/**
|
|
2069
|
-
* Get collection by resolving multi-segment paths through relations
|
|
2070
|
-
* e.g., "authors/70/posts" resolves to the posts collection
|
|
2071
|
-
*/
|
|
2072
|
-
getCollectionByPath(collectionPath) {
|
|
2073
|
-
if (!collectionPath.includes("/")) return this.get(collectionPath);
|
|
2074
|
-
const pathSegments = collectionPath.split("/").filter((p) => p);
|
|
2075
|
-
if (pathSegments.length < 3 || pathSegments.length % 2 === 0) throw new Error(`Invalid relation path: ${collectionPath}. Expected format: collection/id/relation or collection/id/relation/id/relation`);
|
|
2076
|
-
const rootCollectionPath = pathSegments[0];
|
|
2077
|
-
let currentCollection = this.get(rootCollectionPath);
|
|
2078
|
-
if (!currentCollection) throw new Error(`Root collection not found: ${rootCollectionPath}`);
|
|
2079
|
-
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
2080
|
-
const relationKey = pathSegments[i];
|
|
2081
|
-
if (!(0, _rebasepro_types.getDataSourceCapabilities)(currentCollection.engine).supportsRelations) throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);
|
|
2082
|
-
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
2083
|
-
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
2084
|
-
const target = relation.target();
|
|
2085
|
-
const targetRelationKey = relation.relationName || target.slug;
|
|
2086
|
-
const targetSlug = relation.overrides?.slug ?? targetRelationKey;
|
|
2087
|
-
currentCollection = this.get(targetSlug) || this.normalizeCollection(target);
|
|
2088
|
-
if (i + 1 < pathSegments.length) {}
|
|
2089
|
-
}
|
|
2090
|
-
return currentCollection;
|
|
2091
|
-
}
|
|
2092
|
-
getCollections() {
|
|
2093
|
-
if (!this.cachedCollectionsList) this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());
|
|
2094
|
-
return this.cachedCollectionsList;
|
|
2095
|
-
}
|
|
2096
|
-
getRawCollections() {
|
|
2097
|
-
if (!this.cachedRawCollectionsList) this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());
|
|
2098
|
-
return this.cachedRawCollectionsList;
|
|
2099
|
-
}
|
|
2100
|
-
/**
|
|
2101
|
-
* Resolves a multi-segment path like "products/123/locales" and returns
|
|
2102
|
-
* information about the collections and entity IDs along the path
|
|
2103
|
-
*/
|
|
2104
|
-
resolvePathToCollections(path) {
|
|
2105
|
-
const pathSegments = path.split("/").filter((p) => p);
|
|
2106
|
-
if (pathSegments.length === 0) throw new Error(`Invalid path: ${path}`);
|
|
2107
|
-
if (pathSegments.length % 2 !== 1) throw new Error(`Invalid collection path: ${path}. It must have an odd number of segments.`);
|
|
2108
|
-
const collections = [];
|
|
2109
|
-
const entityIds = [];
|
|
2110
|
-
let currentCollection = this.get(pathSegments[0]);
|
|
2111
|
-
if (!currentCollection) throw new Error(`Unknown collection path or slug: ${pathSegments[0]}`);
|
|
2112
|
-
collections.push(currentCollection);
|
|
2113
|
-
for (let i = 1; i < pathSegments.length; i += 2) {
|
|
2114
|
-
const entityId = pathSegments[i];
|
|
2115
|
-
entityIds.push(entityId);
|
|
2116
|
-
if (i + 1 < pathSegments.length) {
|
|
2117
|
-
const subcollectionSlug = pathSegments[i + 1];
|
|
2118
|
-
const subcollections = getSubcollections(currentCollection);
|
|
2119
|
-
if (!subcollections || subcollections.length === 0) throw new Error(`No subcollections found for ${currentCollection.slug} in path: ${path}`);
|
|
2120
|
-
const subcollection = subcollections.find((c) => c.slug === subcollectionSlug);
|
|
2121
|
-
if (!subcollection) throw new Error(`Subcollection '${subcollectionSlug}' not found in ${currentCollection.slug}`);
|
|
2122
|
-
currentCollection = this.get(subcollection.slug) || this.normalizeCollection(subcollection);
|
|
2123
|
-
collections.push(currentCollection);
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
return {
|
|
2127
|
-
collections,
|
|
2128
|
-
entityIds,
|
|
2129
|
-
finalCollection: currentCollection
|
|
2130
|
-
};
|
|
2131
|
-
}
|
|
2132
|
-
};
|
|
2133
|
-
//#endregion
|
|
2134
|
-
//#region src/collections/default-collections.ts
|
|
2135
|
-
/**
|
|
2136
|
-
* Default users collection.
|
|
2137
|
-
*
|
|
2138
|
-
* Prepended to the developer's collections array by the admin and server.
|
|
2139
|
-
* Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
|
|
2140
|
-
* override by defining their own collection with `slug: "users"`.
|
|
2141
|
-
*/
|
|
2142
|
-
var defaultUsersCollection = defineCollection({
|
|
2143
|
-
name: "Users",
|
|
2144
|
-
singularName: "User",
|
|
2145
|
-
slug: "users",
|
|
2146
|
-
auth: true,
|
|
2147
|
-
table: "users",
|
|
2148
|
-
schema: "rebase",
|
|
2149
|
-
icon: "Users",
|
|
2150
|
-
group: "Settings",
|
|
2151
|
-
openEntityMode: "dialog",
|
|
2152
|
-
disableDefaultActions: ["copy"],
|
|
2153
|
-
securityRules: [{
|
|
2154
|
-
operation: "select",
|
|
2155
|
-
roles: ["admin"]
|
|
2156
|
-
}, {
|
|
2157
|
-
operations: [
|
|
2158
|
-
"insert",
|
|
2159
|
-
"update",
|
|
2160
|
-
"delete"
|
|
2161
|
-
],
|
|
2162
|
-
roles: ["admin"]
|
|
2163
|
-
}],
|
|
2164
|
-
sort: ["createdAt", "desc"],
|
|
2165
|
-
properties: {
|
|
2166
|
-
id: {
|
|
2167
|
-
name: "ID",
|
|
2168
|
-
type: "string",
|
|
2169
|
-
isId: "uuid",
|
|
2170
|
-
ui: { readOnly: true }
|
|
2171
|
-
},
|
|
2172
|
-
email: {
|
|
2173
|
-
name: "Email",
|
|
2174
|
-
type: "string",
|
|
2175
|
-
validation: {
|
|
2176
|
-
required: true,
|
|
2177
|
-
unique: true
|
|
2178
|
-
}
|
|
2179
|
-
},
|
|
2180
|
-
displayName: {
|
|
2181
|
-
name: "Name",
|
|
2182
|
-
type: "string",
|
|
2183
|
-
columnName: "display_name",
|
|
2184
|
-
validation: { required: true }
|
|
2185
|
-
},
|
|
2186
|
-
photoURL: {
|
|
2187
|
-
name: "Photo URL",
|
|
2188
|
-
type: "string",
|
|
2189
|
-
columnName: "photo_url",
|
|
2190
|
-
ui: { url: "image" }
|
|
2191
|
-
},
|
|
2192
|
-
roles: {
|
|
2193
|
-
name: "Roles",
|
|
2194
|
-
type: "array",
|
|
2195
|
-
columnType: "text[]",
|
|
2196
|
-
of: {
|
|
2197
|
-
name: "Role",
|
|
2198
|
-
type: "string",
|
|
2199
|
-
enum: {
|
|
2200
|
-
admin: "Admin",
|
|
2201
|
-
editor: "Editor",
|
|
2202
|
-
viewer: "Viewer"
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
},
|
|
2206
|
-
passwordHash: {
|
|
2207
|
-
name: "Password Hash",
|
|
2208
|
-
type: "string",
|
|
2209
|
-
columnName: "password_hash",
|
|
2210
|
-
ui: {
|
|
2211
|
-
hideFromCollection: true,
|
|
2212
|
-
disabled: { hidden: true }
|
|
2213
|
-
}
|
|
2214
|
-
},
|
|
2215
|
-
emailVerified: {
|
|
2216
|
-
name: "Email Verified",
|
|
2217
|
-
type: "boolean",
|
|
2218
|
-
columnName: "email_verified",
|
|
2219
|
-
defaultValue: false,
|
|
2220
|
-
ui: {
|
|
2221
|
-
hideFromCollection: true,
|
|
2222
|
-
disabled: { hidden: true }
|
|
2223
|
-
}
|
|
2224
|
-
},
|
|
2225
|
-
emailVerificationToken: {
|
|
2226
|
-
name: "Email Verification Token",
|
|
2227
|
-
type: "string",
|
|
2228
|
-
columnName: "email_verification_token",
|
|
2229
|
-
ui: {
|
|
2230
|
-
hideFromCollection: true,
|
|
2231
|
-
disabled: { hidden: true }
|
|
2232
|
-
}
|
|
2233
|
-
},
|
|
2234
|
-
emailVerificationSentAt: {
|
|
2235
|
-
name: "Email Verification Sent At",
|
|
2236
|
-
type: "date",
|
|
2237
|
-
columnName: "email_verification_sent_at",
|
|
2238
|
-
ui: {
|
|
2239
|
-
hideFromCollection: true,
|
|
2240
|
-
disabled: { hidden: true }
|
|
2241
|
-
}
|
|
2242
|
-
},
|
|
2243
|
-
metadata: {
|
|
2244
|
-
name: "Metadata",
|
|
2245
|
-
type: "map",
|
|
2246
|
-
keyValue: true,
|
|
2247
|
-
properties: {},
|
|
2248
|
-
defaultValue: {},
|
|
2249
|
-
ui: {
|
|
2250
|
-
hideFromCollection: true,
|
|
2251
|
-
disabled: { hidden: true }
|
|
2252
|
-
}
|
|
2253
|
-
},
|
|
2254
|
-
createdAt: {
|
|
2255
|
-
name: "Created At",
|
|
2256
|
-
type: "date",
|
|
2257
|
-
columnName: "created_at",
|
|
2258
|
-
autoValue: "on_create",
|
|
2259
|
-
ui: { readOnly: true }
|
|
2260
|
-
},
|
|
2261
|
-
updatedAt: {
|
|
2262
|
-
name: "Updated At",
|
|
2263
|
-
type: "date",
|
|
2264
|
-
columnName: "updated_at",
|
|
2265
|
-
autoValue: "on_update",
|
|
2266
|
-
ui: {
|
|
2267
|
-
hideFromCollection: true,
|
|
2268
|
-
disabled: { hidden: true }
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
},
|
|
2272
|
-
listProperties: [
|
|
2273
|
-
"displayName",
|
|
2274
|
-
"email",
|
|
2275
|
-
"roles",
|
|
2276
|
-
"createdAt"
|
|
2277
|
-
],
|
|
2278
|
-
propertiesOrder: [
|
|
2279
|
-
"id",
|
|
2280
|
-
"email",
|
|
2281
|
-
"displayName",
|
|
2282
|
-
"roles",
|
|
2283
|
-
"createdAt"
|
|
2284
|
-
]
|
|
2285
|
-
});
|
|
2286
|
-
//#endregion
|
|
2287
|
-
//#region src/data/query_builder.ts
|
|
2288
|
-
function or(...conditions) {
|
|
2289
|
-
return {
|
|
2290
|
-
type: "or",
|
|
2291
|
-
conditions
|
|
2292
|
-
};
|
|
2293
|
-
}
|
|
2294
|
-
function and(...conditions) {
|
|
2295
|
-
return {
|
|
2296
|
-
type: "and",
|
|
2297
|
-
conditions
|
|
2298
|
-
};
|
|
2299
|
-
}
|
|
2300
|
-
function cond(column, operator, value) {
|
|
2301
|
-
return {
|
|
2302
|
-
column,
|
|
2303
|
-
operator,
|
|
2304
|
-
value
|
|
2305
|
-
};
|
|
2306
|
-
}
|
|
2307
|
-
var QueryBuilder = class {
|
|
2308
|
-
collection;
|
|
2309
|
-
params = { where: {} };
|
|
2310
|
-
constructor(collection) {
|
|
2311
|
-
this.collection = collection;
|
|
2312
|
-
}
|
|
2313
|
-
where(columnOrCondition, operator, value) {
|
|
2314
|
-
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
2315
|
-
this.params.logical = columnOrCondition;
|
|
2316
|
-
return this;
|
|
2317
|
-
}
|
|
2318
|
-
if (!this.params.where) this.params.where = {};
|
|
2319
|
-
const column = columnOrCondition;
|
|
2320
|
-
const condition = [operator, value];
|
|
2321
|
-
const existing = this.params.where[column];
|
|
2322
|
-
if (existing === void 0) this.params.where[column] = condition;
|
|
2323
|
-
else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
|
|
2324
|
-
else {
|
|
2325
|
-
let firstCondition;
|
|
2326
|
-
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
|
|
2327
|
-
else firstCondition = ["==", existing];
|
|
2328
|
-
this.params.where[column] = [firstCondition, condition];
|
|
2329
|
-
}
|
|
2330
|
-
return this;
|
|
2331
|
-
}
|
|
2332
|
-
/**
|
|
2333
|
-
* Order the results by a specific column.
|
|
2334
|
-
* @example
|
|
2335
|
-
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
2336
|
-
*/
|
|
2337
|
-
orderBy(column, direction = "asc") {
|
|
2338
|
-
this.params.orderBy = [column, direction];
|
|
2339
|
-
return this;
|
|
2340
|
-
}
|
|
2341
|
-
/**
|
|
2342
|
-
* Limit the number of results returned.
|
|
2343
|
-
*/
|
|
2344
|
-
limit(count) {
|
|
2345
|
-
this.params.limit = count;
|
|
2346
|
-
return this;
|
|
2347
|
-
}
|
|
2348
|
-
/**
|
|
2349
|
-
* Skip the first N results.
|
|
2350
|
-
*/
|
|
2351
|
-
offset(count) {
|
|
2352
|
-
this.params.offset = count;
|
|
2353
|
-
return this;
|
|
2354
|
-
}
|
|
2355
|
-
/**
|
|
2356
|
-
* Set a free-text search string if supported by the backend.
|
|
2357
|
-
*/
|
|
2358
|
-
search(searchString) {
|
|
2359
|
-
this.params.searchString = searchString;
|
|
2360
|
-
return this;
|
|
2361
|
-
}
|
|
2362
|
-
/**
|
|
2363
|
-
* Include related entities in the response.
|
|
2364
|
-
* Relations will be populated with full entity data instead of just IDs.
|
|
2365
|
-
*
|
|
2366
|
-
* @param relations - Relation names to include, or "*" for all.
|
|
2367
|
-
* @example
|
|
2368
|
-
* // Include specific relations
|
|
2369
|
-
* client.data.posts.include("tags", "author").find()
|
|
2370
|
-
*
|
|
2371
|
-
* // Include all relations
|
|
2372
|
-
* client.data.posts.include("*").find()
|
|
2373
|
-
*/
|
|
2374
|
-
include(...relations) {
|
|
2375
|
-
this.params.include = relations;
|
|
2376
|
-
return this;
|
|
2377
|
-
}
|
|
2378
|
-
/**
|
|
2379
|
-
* Execute the find query and return the results.
|
|
2380
|
-
*/
|
|
2381
|
-
async find() {
|
|
2382
|
-
return this.collection.find(this.params);
|
|
2383
|
-
}
|
|
2384
|
-
/**
|
|
2385
|
-
* Listen to realtime updates matching this query.
|
|
2386
|
-
*/
|
|
2387
|
-
listen(onUpdate, onError) {
|
|
2388
|
-
if (!this.collection.listen) throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
|
|
2389
|
-
return this.collection.listen(this.params, onUpdate, onError);
|
|
2390
|
-
}
|
|
2391
|
-
};
|
|
2392
|
-
//#endregion
|
|
2393
|
-
//#region src/data/filter-dialect.ts
|
|
2394
|
-
/**
|
|
2395
|
-
* REST wire-format adapter for the unified filter system.
|
|
2396
|
-
*
|
|
2397
|
-
* This module is the ONLY code in the entire codebase that knows about
|
|
2398
|
-
* PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
|
|
2399
|
-
* Everything else speaks `FilterValues` exclusively.
|
|
2400
|
-
*
|
|
2401
|
-
* Wire-format values are always strings — the wire format carries no type
|
|
2402
|
-
* metadata, so type coercion is the responsibility of the server-side data
|
|
2403
|
-
* driver which has access to the collection schema.
|
|
2404
|
-
*
|
|
2405
|
-
* Commas inside list values are backslash-escaped (`\,`), and literal
|
|
2406
|
-
* backslashes are escaped as `\\`.
|
|
2407
|
-
*
|
|
2408
|
-
* @module
|
|
2409
|
-
*/
|
|
2410
|
-
/**
|
|
2411
|
-
* Serialize a JS value to its querystring representation.
|
|
2412
|
-
* `null` is serialized as the literal string `"null"`.
|
|
2413
|
-
* Relation values (`EntityRelation` instances or `{ __type: "relation", id, path }`
|
|
2414
|
-
* objects) are serialized as their raw id — the wire format only carries the
|
|
2415
|
-
* value to compare against the FK column.
|
|
2416
|
-
*/
|
|
2417
|
-
function stringifyValue(value) {
|
|
2418
|
-
if (value === null) return "null";
|
|
2419
|
-
const relation = normalizeToEntityRelation(value);
|
|
2420
|
-
if (relation) return String(relation.id);
|
|
2421
|
-
return String(value);
|
|
2422
|
-
}
|
|
2423
|
-
/**
|
|
2424
|
-
* Escape a single list item for the wire format.
|
|
2425
|
-
* `\` → `\\`, `,` → `\,`
|
|
2426
|
-
*/
|
|
2427
|
-
function escapeListItem(value) {
|
|
2428
|
-
return value.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
|
|
2429
|
-
}
|
|
2430
|
-
/**
|
|
2431
|
-
* Unescape a single list item from the wire format.
|
|
2432
|
-
* `\\` → `\`, `\,` → `,`
|
|
2433
|
-
*/
|
|
2434
|
-
function unescapeListItem(value) {
|
|
2435
|
-
let result = "";
|
|
2436
|
-
for (let i = 0; i < value.length; i++) if (value[i] === "\\" && i + 1 < value.length) {
|
|
2437
|
-
result += value[i + 1];
|
|
2438
|
-
i++;
|
|
2439
|
-
} else result += value[i];
|
|
2440
|
-
return result;
|
|
2441
|
-
}
|
|
2442
|
-
/**
|
|
2443
|
-
* Split a parenthesized list string on unescaped commas.
|
|
2444
|
-
* Input is the content between `(` and `)`.
|
|
2445
|
-
*
|
|
2446
|
-
* @example
|
|
2447
|
-
* splitListItems("admin,editor") // ["admin", "editor"]
|
|
2448
|
-
* splitListItems("hello\\, world,foo") // ["hello, world", "foo"]
|
|
2449
|
-
*/
|
|
2450
|
-
function splitListItems(inner) {
|
|
2451
|
-
const items = [];
|
|
2452
|
-
let current = "";
|
|
2453
|
-
for (let i = 0; i < inner.length; i++) if (inner[i] === "\\" && i + 1 < inner.length) {
|
|
2454
|
-
current += inner[i] + inner[i + 1];
|
|
2455
|
-
i++;
|
|
2456
|
-
} else if (inner[i] === ",") {
|
|
2457
|
-
items.push(unescapeListItem(current));
|
|
2458
|
-
current = "";
|
|
2459
|
-
} else current += inner[i];
|
|
2460
|
-
items.push(unescapeListItem(current));
|
|
2461
|
-
return items;
|
|
2462
|
-
}
|
|
2463
|
-
var REST_OP_LOOKUP = _rebasepro_types.REST_TO_CANONICAL;
|
|
2464
|
-
var CANONICAL_OP_LOOKUP = _rebasepro_types.CANONICAL_TO_REST;
|
|
2465
|
-
/**
|
|
2466
|
-
* Serialize a single canonical condition tuple to a PostgREST dot-string.
|
|
2467
|
-
*
|
|
2468
|
-
* Throws `TypeError` if the input is not a valid `[WhereFilterOp, unknown]` tuple.
|
|
2469
|
-
*
|
|
2470
|
-
* @example
|
|
2471
|
-
* serializeTuple(["==", "active"]) // "eq.active"
|
|
2472
|
-
* serializeTuple(["in", ["admin","editor"]]) // "in.(admin,editor)"
|
|
2473
|
-
* serializeTuple([">=", 18]) // "gte.18"
|
|
2474
|
-
*/
|
|
2475
|
-
function serializeTuple(tuple) {
|
|
2476
|
-
if (!Array.isArray(tuple) || tuple.length !== 2) throw new TypeError(`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`);
|
|
2477
|
-
const [op, value] = tuple;
|
|
2478
|
-
if (typeof op !== "string") throw new TypeError(`serializeTuple: operator must be a string, got ${typeof op}`);
|
|
2479
|
-
const restOp = CANONICAL_OP_LOOKUP[op];
|
|
2480
|
-
if (!restOp) throw new TypeError(`serializeTuple: unknown operator "${op}". Valid operators: ${Object.keys(_rebasepro_types.CANONICAL_TO_REST).join(", ")}`);
|
|
2481
|
-
if (Array.isArray(value)) return `${restOp}.(${value.map((v) => escapeListItem(stringifyValue(v))).join(",")})`;
|
|
2482
|
-
return `${restOp}.${stringifyValue(value)}`;
|
|
2483
|
-
}
|
|
2484
|
-
/**
|
|
2485
|
-
* Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
|
|
2486
|
-
* querystring record.
|
|
2487
|
-
*
|
|
2488
|
-
* - Canonical `[WhereFilterOp, value]` tuples are serialized strictly.
|
|
2489
|
-
* - Pre-serialized PostgREST strings (e.g. `"eq.published"`) are passed through.
|
|
2490
|
-
* - Single conditions produce a string value.
|
|
2491
|
-
* - Multiple conditions on the same field produce a string array (repeated params).
|
|
2492
|
-
*
|
|
2493
|
-
* @example
|
|
2494
|
-
* serializeFilter({ status: ["==", "active"] })
|
|
2495
|
-
* // → { status: "eq.active" }
|
|
2496
|
-
*
|
|
2497
|
-
* serializeFilter({ age: [[">=", 18], ["<", 65]] })
|
|
2498
|
-
* // → { age: ["gte.18", "lt.65"] }
|
|
2499
|
-
*
|
|
2500
|
-
* // Pre-serialized strings pass through unchanged:
|
|
2501
|
-
* serializeFilter({ status: "eq.published" })
|
|
2502
|
-
* // → { status: "eq.published" }
|
|
2503
|
-
*/
|
|
2504
|
-
function serializeFilter(filter) {
|
|
2505
|
-
const result = {};
|
|
2506
|
-
for (const [field, condition] of Object.entries(filter)) {
|
|
2507
|
-
if (condition === void 0) continue;
|
|
2508
|
-
if (typeof condition === "string") {
|
|
2509
|
-
result[field] = condition;
|
|
2510
|
-
continue;
|
|
2511
|
-
}
|
|
2512
|
-
if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
|
|
2513
|
-
else result[field] = serializeTuple(condition);
|
|
2514
|
-
}
|
|
2515
|
-
return result;
|
|
2516
|
-
}
|
|
2517
|
-
/**
|
|
2518
|
-
* Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
|
|
2519
|
-
*
|
|
2520
|
-
* All values are returned as strings — the wire format carries no type
|
|
2521
|
-
* metadata, so coercion is the data driver's responsibility.
|
|
2522
|
-
*
|
|
2523
|
-
* If the string doesn't match a known operator prefix, it falls back to
|
|
2524
|
-
* `["==", originalString]` (treating the whole string as an equality value).
|
|
2525
|
-
* This intentional defense handles values like `"user@host.com"` or
|
|
2526
|
-
* `"1.2.3"` that happen to contain dots.
|
|
2527
|
-
*/
|
|
2528
|
-
function deserializeSingle(raw) {
|
|
2529
|
-
const dotIndex = raw.indexOf(".");
|
|
2530
|
-
if (dotIndex === -1) return ["==", raw];
|
|
2531
|
-
const prefix = raw.substring(0, dotIndex);
|
|
2532
|
-
const rest = raw.substring(dotIndex + 1);
|
|
2533
|
-
const canonicalOp = REST_OP_LOOKUP[prefix];
|
|
2534
|
-
if (!canonicalOp) return ["==", raw];
|
|
2535
|
-
if (_rebasepro_types.NULL_OPS.has(canonicalOp)) return [canonicalOp, null];
|
|
2536
|
-
if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, splitListItems(rest.slice(1, -1))];
|
|
2537
|
-
return [canonicalOp, rest];
|
|
2538
|
-
}
|
|
2539
|
-
/**
|
|
2540
|
-
* Convert a PostgREST-style querystring record to `FilterValues`.
|
|
2541
|
-
*
|
|
2542
|
-
* - String values are parsed as single conditions.
|
|
2543
|
-
* - String arrays (repeated query params) become multiple conditions on the same field.
|
|
2544
|
-
*
|
|
2545
|
-
* @example
|
|
2546
|
-
* deserializeFilter({ status: "eq.active" })
|
|
2547
|
-
* // → { status: ["==", "active"] }
|
|
2548
|
-
*
|
|
2549
|
-
* deserializeFilter({ age: ["gte.18", "lt.65"] })
|
|
2550
|
-
* // → { age: [[">=", "18"], ["<", "65"]] }
|
|
2551
|
-
*/
|
|
2552
|
-
function deserializeFilter(query) {
|
|
2553
|
-
const result = {};
|
|
2554
|
-
for (const [field, raw] of Object.entries(query)) {
|
|
2555
|
-
if (raw === void 0) continue;
|
|
2556
|
-
if (Array.isArray(raw) && raw.length === 2 && typeof raw[0] === "string" && (0, _rebasepro_types.toCanonicalOp)(raw[0]) === raw[0]) {
|
|
2557
|
-
result[field] = raw;
|
|
2558
|
-
continue;
|
|
2559
|
-
}
|
|
2560
|
-
if (Array.isArray(raw)) {
|
|
2561
|
-
if (raw.length === 0) continue;
|
|
2562
|
-
if (Array.isArray(raw[0]) && raw[0].length === 2 && typeof raw[0][0] === "string" && (0, _rebasepro_types.toCanonicalOp)(raw[0][0]) === raw[0][0]) {
|
|
2563
|
-
result[field] = raw;
|
|
2564
|
-
continue;
|
|
2565
|
-
}
|
|
2566
|
-
if (raw.length === 1) result[field] = typeof raw[0] === "string" ? deserializeSingle(raw[0]) : ["==", raw[0]];
|
|
2567
|
-
else if (typeof raw[0] === "string" && raw[0].includes(".")) result[field] = raw.map((r) => typeof r === "string" ? deserializeSingle(r) : ["==", r]);
|
|
2568
|
-
else result[field] = ["in", raw];
|
|
2569
|
-
} else if (typeof raw === "string") result[field] = deserializeSingle(raw);
|
|
2570
|
-
else result[field] = ["==", raw];
|
|
2571
|
-
}
|
|
2572
|
-
return result;
|
|
2573
|
-
}
|
|
2574
|
-
/**
|
|
2575
|
-
* Serialize a `LogicalCondition` or `FilterCondition` to its wire-format string.
|
|
2576
|
-
*
|
|
2577
|
-
* @example
|
|
2578
|
-
* serializeLogicalCondition({ column: "status", operator: "==", value: "active" })
|
|
2579
|
-
* // → "status.eq.active"
|
|
2580
|
-
*
|
|
2581
|
-
* serializeLogicalCondition({ type: "or", conditions: [...] })
|
|
2582
|
-
* // → "or(status.eq.active,status.eq.pending)"
|
|
2583
|
-
*/
|
|
2584
|
-
function serializeLogicalCondition(cond) {
|
|
2585
|
-
if ("type" in cond) {
|
|
2586
|
-
const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
|
|
2587
|
-
return `${cond.type}(${inner})`;
|
|
2588
|
-
}
|
|
2589
|
-
const restOp = CANONICAL_OP_LOOKUP[cond.operator] ?? "eq";
|
|
2590
|
-
if (Array.isArray(cond.value)) {
|
|
2591
|
-
const items = cond.value.map((v) => escapeListItem(stringifyValue(v))).join(",");
|
|
2592
|
-
return `${cond.column}.${restOp}.(${items})`;
|
|
2593
|
-
}
|
|
2594
|
-
return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
|
|
2595
|
-
}
|
|
2596
|
-
/**
|
|
2597
|
-
* Parse a logical condition wire-format string back into a
|
|
2598
|
-
* `LogicalCondition` or `FilterCondition`.
|
|
2599
|
-
*
|
|
2600
|
-
* @example
|
|
2601
|
-
* deserializeLogicalCondition("status.eq.active")
|
|
2602
|
-
* // → { column: "status", operator: "==", value: "active" }
|
|
2603
|
-
*
|
|
2604
|
-
* deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
|
|
2605
|
-
* // → { type: "or", conditions: [...] }
|
|
2606
|
-
*/
|
|
2607
|
-
function deserializeLogicalCondition(str) {
|
|
2608
|
-
const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
|
|
2609
|
-
if (logicalMatch) {
|
|
2610
|
-
const type = logicalMatch[1];
|
|
2611
|
-
const innerStr = logicalMatch[2];
|
|
2612
|
-
const conditions = [];
|
|
2613
|
-
let depth = 0;
|
|
2614
|
-
let start = 0;
|
|
2615
|
-
for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
|
|
2616
|
-
else if (innerStr[i] === ")") depth--;
|
|
2617
|
-
else if (innerStr[i] === "," && depth === 0) {
|
|
2618
|
-
conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
|
|
2619
|
-
start = i + 1;
|
|
2620
|
-
}
|
|
2621
|
-
conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
|
|
2622
|
-
return {
|
|
2623
|
-
type,
|
|
2624
|
-
conditions
|
|
2625
|
-
};
|
|
2626
|
-
}
|
|
2627
|
-
const firstDot = str.indexOf(".");
|
|
2628
|
-
if (firstDot === -1) return {
|
|
2629
|
-
column: str,
|
|
2630
|
-
operator: "==",
|
|
2631
|
-
value: true
|
|
2632
|
-
};
|
|
2633
|
-
const column = str.substring(0, firstDot);
|
|
2634
|
-
const rest = str.substring(firstDot + 1);
|
|
2635
|
-
const secondDot = rest.indexOf(".");
|
|
2636
|
-
if (secondDot === -1) return {
|
|
2637
|
-
column,
|
|
2638
|
-
operator: "==",
|
|
2639
|
-
value: rest
|
|
2640
|
-
};
|
|
2641
|
-
const opStr = rest.substring(0, secondDot);
|
|
2642
|
-
const valueStr = rest.substring(secondDot + 1);
|
|
2643
|
-
const operator = (0, _rebasepro_types.toCanonicalOp)(opStr) ?? "==";
|
|
2644
|
-
if (valueStr.startsWith("(") && valueStr.endsWith(")")) return {
|
|
2645
|
-
column,
|
|
2646
|
-
operator,
|
|
2647
|
-
value: splitListItems(valueStr.slice(1, -1))
|
|
2648
|
-
};
|
|
2649
|
-
return {
|
|
2650
|
-
column,
|
|
2651
|
-
operator,
|
|
2652
|
-
value: valueStr
|
|
2653
|
-
};
|
|
2654
|
-
}
|
|
2655
|
-
//#endregion
|
|
2656
|
-
//#region src/data/buildRebaseData.ts
|
|
2657
|
-
/**
|
|
2658
|
-
* Convert a flat REST record (e.g. from RestFetchService) to Entity<M> format.
|
|
2659
|
-
* Mirrors the client SDK's rowToEntity conversion.
|
|
2660
|
-
*/
|
|
2661
|
-
function rowToEntity(row, slug) {
|
|
2662
|
-
return {
|
|
2663
|
-
id: row.id,
|
|
2664
|
-
path: slug,
|
|
2665
|
-
values: row
|
|
2666
|
-
};
|
|
2667
|
-
}
|
|
2668
|
-
function createDriverAccessor(driver, slug) {
|
|
2669
|
-
const accessor = {
|
|
2670
|
-
async find(params) {
|
|
2671
|
-
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2672
|
-
const limit = params?.limit ?? 20;
|
|
2673
|
-
const offset = params?.offset ?? 0;
|
|
2674
|
-
const fetchService = driver.restFetchService;
|
|
2675
|
-
const rows = fetchService && params?.include && params.include.length > 0 ? await fetchService.fetchCollectionForRest(slug, {
|
|
2676
|
-
filter,
|
|
2677
|
-
limit: params?.limit,
|
|
2678
|
-
offset: params?.offset,
|
|
2679
|
-
orderBy: params?.orderBy?.[0],
|
|
2680
|
-
order: params?.orderBy?.[1],
|
|
2681
|
-
searchString: params?.searchString
|
|
2682
|
-
}, params.include) : await driver.fetchCollection({
|
|
2683
|
-
path: slug,
|
|
2684
|
-
limit: params?.limit,
|
|
2685
|
-
offset: params?.offset,
|
|
2686
|
-
filter,
|
|
2687
|
-
orderBy: params?.orderBy?.[0],
|
|
2688
|
-
order: params?.orderBy?.[1],
|
|
2689
|
-
searchString: params?.searchString
|
|
2690
|
-
});
|
|
2691
|
-
let total = rows.length + offset;
|
|
2692
|
-
let hasMore = rows.length >= limit;
|
|
2693
|
-
if (driver.count) {
|
|
2694
|
-
total = await driver.count({
|
|
2695
|
-
path: slug,
|
|
2696
|
-
filter
|
|
2697
|
-
});
|
|
2698
|
-
hasMore = offset + rows.length < total;
|
|
2699
|
-
}
|
|
2700
|
-
return {
|
|
2701
|
-
data: rows.map((row) => rowToEntity(row, slug)),
|
|
2702
|
-
meta: {
|
|
2703
|
-
total,
|
|
2704
|
-
limit,
|
|
2705
|
-
offset,
|
|
2706
|
-
hasMore
|
|
2707
|
-
}
|
|
2708
|
-
};
|
|
2709
|
-
},
|
|
2710
|
-
async findById(id) {
|
|
2711
|
-
const row = await driver.fetchOne({
|
|
2712
|
-
path: slug,
|
|
2713
|
-
id
|
|
2714
|
-
});
|
|
2715
|
-
return row ? rowToEntity(row, slug) : void 0;
|
|
2716
|
-
},
|
|
2717
|
-
async create(data, id) {
|
|
2718
|
-
return rowToEntity(await driver.save({
|
|
2719
|
-
path: slug,
|
|
2720
|
-
values: data,
|
|
2721
|
-
id,
|
|
2722
|
-
status: "new"
|
|
2723
|
-
}), slug);
|
|
2724
|
-
},
|
|
2725
|
-
async update(id, data) {
|
|
2726
|
-
return rowToEntity(await driver.save({
|
|
2727
|
-
path: slug,
|
|
2728
|
-
values: data,
|
|
2729
|
-
id,
|
|
2730
|
-
status: "existing"
|
|
2731
|
-
}), slug);
|
|
2732
|
-
},
|
|
2733
|
-
async delete(id) {
|
|
2734
|
-
return driver.delete({ row: {
|
|
2735
|
-
id,
|
|
2736
|
-
path: slug,
|
|
2737
|
-
values: {}
|
|
2738
|
-
} });
|
|
2739
|
-
},
|
|
2740
|
-
count: driver.count ? async (params) => {
|
|
2741
|
-
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2742
|
-
return driver.count({
|
|
2743
|
-
path: slug,
|
|
2744
|
-
filter
|
|
2745
|
-
});
|
|
2746
|
-
} : void 0,
|
|
2747
|
-
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
2748
|
-
const limit = params?.limit ?? 20;
|
|
2749
|
-
const offset = params?.offset ?? 0;
|
|
2750
|
-
return driver.listenCollection({
|
|
2751
|
-
path: slug,
|
|
2752
|
-
limit: params?.limit,
|
|
2753
|
-
offset: params?.offset,
|
|
2754
|
-
filter: params?.where,
|
|
2755
|
-
orderBy: params?.orderBy?.[0],
|
|
2756
|
-
order: params?.orderBy?.[1],
|
|
2757
|
-
searchString: params?.searchString,
|
|
2758
|
-
onUpdate: (entities) => {
|
|
2759
|
-
onUpdate({
|
|
2760
|
-
data: entities.map((row) => rowToEntity(row, slug)),
|
|
2761
|
-
meta: {
|
|
2762
|
-
total: entities.length,
|
|
2763
|
-
limit,
|
|
2764
|
-
offset,
|
|
2765
|
-
hasMore: entities.length >= limit
|
|
2766
|
-
}
|
|
2767
|
-
});
|
|
2768
|
-
},
|
|
2769
|
-
onError
|
|
2770
|
-
});
|
|
2771
|
-
} : void 0,
|
|
2772
|
-
listenById: driver.listenOne ? (id, onUpdate, onError) => {
|
|
2773
|
-
return driver.listenOne({
|
|
2774
|
-
path: slug,
|
|
2775
|
-
id,
|
|
2776
|
-
onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug) : void 0),
|
|
2777
|
-
onError
|
|
2778
|
-
});
|
|
2779
|
-
} : void 0,
|
|
2780
|
-
where(columnOrCondition, operator, value) {
|
|
2781
|
-
const builder = new QueryBuilder(accessor);
|
|
2782
|
-
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
2783
|
-
return builder.where(columnOrCondition, operator, value);
|
|
2784
|
-
},
|
|
2785
|
-
orderBy(column, ascending) {
|
|
2786
|
-
return new QueryBuilder(accessor).orderBy(column, ascending);
|
|
2787
|
-
},
|
|
2788
|
-
limit(count) {
|
|
2789
|
-
return new QueryBuilder(accessor).limit(count);
|
|
2790
|
-
},
|
|
2791
|
-
offset(count) {
|
|
2792
|
-
return new QueryBuilder(accessor).offset(count);
|
|
2793
|
-
},
|
|
2794
|
-
search(searchString) {
|
|
2795
|
-
return new QueryBuilder(accessor).search(searchString);
|
|
2796
|
-
},
|
|
2797
|
-
include(...relations) {
|
|
2798
|
-
return new QueryBuilder(accessor).include(...relations);
|
|
2799
|
-
}
|
|
2800
|
-
};
|
|
2801
|
-
return accessor;
|
|
2802
|
-
}
|
|
2803
|
-
/**
|
|
2804
|
-
* Build a `RebaseData` object from a `DataDriver` using JavaScript Proxy.
|
|
2805
|
-
*
|
|
2806
|
-
* This is the key bridge: any property access like `data.products` returns
|
|
2807
|
-
* a `CollectionAccessor` backed by the underlying DataDriver, without
|
|
2808
|
-
* needing per-collection code generation.
|
|
2809
|
-
*
|
|
2810
|
-
* @example
|
|
2811
|
-
* const data = buildRebaseData(driver);
|
|
2812
|
-
* await data.products.create({ name: "Camera", price: 299 });
|
|
2813
|
-
* const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
|
|
2814
|
-
*/
|
|
2815
|
-
function buildRebaseData(driver) {
|
|
2816
|
-
const cache = /* @__PURE__ */ new Map();
|
|
2817
|
-
function getAccessor(slug) {
|
|
2818
|
-
let accessor = cache.get(slug);
|
|
2819
|
-
if (!accessor) {
|
|
2820
|
-
accessor = createDriverAccessor(driver, slug);
|
|
2821
|
-
cache.set(slug, accessor);
|
|
2822
|
-
}
|
|
2823
|
-
return accessor;
|
|
2824
|
-
}
|
|
2825
|
-
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
2826
|
-
if (prop === "collection") return getAccessor;
|
|
2827
|
-
if (typeof prop === "symbol") return void 0;
|
|
2828
|
-
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
2829
|
-
return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
|
|
2830
|
-
} });
|
|
2831
|
-
}
|
|
2832
|
-
/**
|
|
2833
|
-
* Unwrap a Entity into a flat row. `rowToEntity` stores the whole flat row
|
|
2834
|
-
* (id included) under `.values`, so this is just that payload.
|
|
2835
|
-
*/
|
|
2836
|
-
function entityToRow(entity) {
|
|
2837
|
-
return entity.values;
|
|
2838
|
-
}
|
|
2839
|
-
/**
|
|
2840
|
-
* Fluent query builder for the flat SDK data layer. Mirrors {@link QueryBuilder}
|
|
2841
|
-
* but resolves to `FindResult<M>` (flat rows) instead of Entity-wrapped
|
|
2842
|
-
* `FindResponse<M>`.
|
|
2843
|
-
*/
|
|
2844
|
-
var SdkQueryBuilder = class {
|
|
2845
|
-
client;
|
|
2846
|
-
params = { where: {} };
|
|
2847
|
-
constructor(client) {
|
|
2848
|
-
this.client = client;
|
|
2849
|
-
}
|
|
2850
|
-
where(columnOrCondition, operator, value) {
|
|
2851
|
-
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
2852
|
-
this.params.logical = columnOrCondition;
|
|
2853
|
-
return this;
|
|
2854
|
-
}
|
|
2855
|
-
if (!this.params.where) this.params.where = {};
|
|
2856
|
-
const column = columnOrCondition;
|
|
2857
|
-
const condition = [operator, value];
|
|
2858
|
-
const existing = this.params.where[column];
|
|
2859
|
-
if (existing === void 0) this.params.where[column] = condition;
|
|
2860
|
-
else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
|
|
2861
|
-
else {
|
|
2862
|
-
let firstCondition;
|
|
2863
|
-
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
|
|
2864
|
-
else firstCondition = ["==", existing];
|
|
2865
|
-
this.params.where[column] = [firstCondition, condition];
|
|
2866
|
-
}
|
|
2867
|
-
return this;
|
|
2868
|
-
}
|
|
2869
|
-
orderBy(column, direction = "asc") {
|
|
2870
|
-
this.params.orderBy = [column, direction];
|
|
2871
|
-
return this;
|
|
2872
|
-
}
|
|
2873
|
-
limit(count) {
|
|
2874
|
-
this.params.limit = count;
|
|
2875
|
-
return this;
|
|
2876
|
-
}
|
|
2877
|
-
offset(count) {
|
|
2878
|
-
this.params.offset = count;
|
|
2879
|
-
return this;
|
|
2880
|
-
}
|
|
2881
|
-
search(searchString) {
|
|
2882
|
-
this.params.searchString = searchString;
|
|
2883
|
-
return this;
|
|
2884
|
-
}
|
|
2885
|
-
include(...relations) {
|
|
2886
|
-
this.params.include = relations;
|
|
2887
|
-
return this;
|
|
2888
|
-
}
|
|
2889
|
-
async find() {
|
|
2890
|
-
return this.client.find(this.params);
|
|
2891
|
-
}
|
|
2892
|
-
async count() {
|
|
2893
|
-
return this.client.count ? this.client.count(this.params) : 0;
|
|
2894
|
-
}
|
|
2895
|
-
listen(onUpdate, onError) {
|
|
2896
|
-
if (!this.client.listen) throw new Error("Listen is only available when the driver supports realtime.");
|
|
2897
|
-
return this.client.listen(this.params, onUpdate, onError);
|
|
2898
|
-
}
|
|
2899
|
-
};
|
|
2900
|
-
/**
|
|
2901
|
-
* Wrap a Entity-shaped {@link CollectionAccessor} into a flat
|
|
2902
|
-
* {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
|
|
2903
|
-
* so the backend SDK is byte-for-byte the same shape as the frontend client.
|
|
2904
|
-
*/
|
|
2905
|
-
function toSdkCollectionClient(snap) {
|
|
2906
|
-
const client = {
|
|
2907
|
-
async find(params) {
|
|
2908
|
-
const res = await snap.find(params);
|
|
2909
|
-
return {
|
|
2910
|
-
data: res.data.map(entityToRow),
|
|
2911
|
-
meta: res.meta
|
|
2912
|
-
};
|
|
2913
|
-
},
|
|
2914
|
-
async findById(id) {
|
|
2915
|
-
const s = await snap.findById(id);
|
|
2916
|
-
return s ? entityToRow(s) : void 0;
|
|
2917
|
-
},
|
|
2918
|
-
async create(data, id) {
|
|
2919
|
-
return entityToRow(await snap.create(data, id));
|
|
2920
|
-
},
|
|
2921
|
-
async update(id, data) {
|
|
2922
|
-
return entityToRow(await snap.update(id, data));
|
|
2923
|
-
},
|
|
2924
|
-
delete(id) {
|
|
2925
|
-
return snap.delete(id);
|
|
2926
|
-
},
|
|
2927
|
-
count: snap.count ? (params) => snap.count(params) : void 0,
|
|
2928
|
-
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
2929
|
-
data: res.data.map(entityToRow),
|
|
2930
|
-
meta: res.meta
|
|
2931
|
-
}), onError) : void 0,
|
|
2932
|
-
listenById: snap.listenById ? (id, onUpdate, onError) => snap.listenById(id, (s) => onUpdate(s ? entityToRow(s) : void 0), onError) : void 0,
|
|
2933
|
-
where(columnOrCondition, operator, value) {
|
|
2934
|
-
const builder = new SdkQueryBuilder(client);
|
|
2935
|
-
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
2936
|
-
return builder.where(columnOrCondition, operator, value);
|
|
2937
|
-
},
|
|
2938
|
-
orderBy: (column, direction) => new SdkQueryBuilder(client).orderBy(column, direction),
|
|
2939
|
-
limit: (count) => new SdkQueryBuilder(client).limit(count),
|
|
2940
|
-
offset: (count) => new SdkQueryBuilder(client).offset(count),
|
|
2941
|
-
search: (searchString) => new SdkQueryBuilder(client).search(searchString),
|
|
2942
|
-
include: (...relations) => new SdkQueryBuilder(client).include(...relations)
|
|
2943
|
-
};
|
|
2944
|
-
return client;
|
|
2945
|
-
}
|
|
2946
|
-
/**
|
|
2947
|
-
* Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
|
|
2948
|
-
* {@link CollectionAccessor}. Every returned row is re-wrapped into the
|
|
2949
|
-
* `{ id, path, values }` view-model the admin CMS renders.
|
|
2950
|
-
*/
|
|
2951
|
-
function toEntityAccessor(sdk, slug) {
|
|
2952
|
-
const accessor = {
|
|
2953
|
-
async find(params) {
|
|
2954
|
-
const res = await sdk.find(params);
|
|
2955
|
-
return {
|
|
2956
|
-
data: res.data.map((row) => rowToEntity(row, slug)),
|
|
2957
|
-
meta: res.meta
|
|
2958
|
-
};
|
|
2959
|
-
},
|
|
2960
|
-
async findById(id) {
|
|
2961
|
-
const row = await sdk.findById(id);
|
|
2962
|
-
return row ? rowToEntity(row, slug) : void 0;
|
|
2963
|
-
},
|
|
2964
|
-
async create(data, id) {
|
|
2965
|
-
return rowToEntity(await sdk.create(data, id), slug);
|
|
2966
|
-
},
|
|
2967
|
-
async update(id, data) {
|
|
2968
|
-
const row = await sdk.update(id, data);
|
|
2969
|
-
if (!row) throw new Error(`Update returned no data for id ${id}`);
|
|
2970
|
-
return rowToEntity(row, slug);
|
|
2971
|
-
},
|
|
2972
|
-
delete(id) {
|
|
2973
|
-
return sdk.delete(id);
|
|
2974
|
-
},
|
|
2975
|
-
count: sdk.count ? (params) => sdk.count(params) : void 0,
|
|
2976
|
-
listen: sdk.listen ? (params, onUpdate, onError) => sdk.listen(params, (res) => onUpdate({
|
|
2977
|
-
data: res.data.map((row) => rowToEntity(row, slug)),
|
|
2978
|
-
meta: res.meta
|
|
2979
|
-
}), onError) : void 0,
|
|
2980
|
-
listenById: sdk.listenById ? (id, onUpdate, onError) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug) : void 0), onError) : void 0,
|
|
2981
|
-
where(columnOrCondition, operator, value) {
|
|
2982
|
-
const builder = new QueryBuilder(accessor);
|
|
2983
|
-
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
2984
|
-
return builder.where(columnOrCondition, operator, value);
|
|
2985
|
-
},
|
|
2986
|
-
orderBy: (column, direction) => new QueryBuilder(accessor).orderBy(column, direction),
|
|
2987
|
-
limit: (count) => new QueryBuilder(accessor).limit(count),
|
|
2988
|
-
offset: (count) => new QueryBuilder(accessor).offset(count),
|
|
2989
|
-
search: (searchString) => new QueryBuilder(accessor).search(searchString),
|
|
2990
|
-
include: (...relations) => new QueryBuilder(accessor).include(...relations)
|
|
2991
|
-
};
|
|
2992
|
-
return accessor;
|
|
2993
|
-
}
|
|
2994
|
-
/**
|
|
2995
|
-
* Wrap a flat {@link RebaseSdkData} into a Entity-shaped {@link RebaseData}.
|
|
2996
|
-
*
|
|
2997
|
-
* This is the **CMS boundary**: the SDK client (`client.data`) returns flat
|
|
2998
|
-
* rows, but the admin renders the `Entity` view-model (`entity.values.*`).
|
|
2999
|
-
* `core/Rebase.tsx` wraps `client.data` through this before handing it to the
|
|
3000
|
-
* CMS `RebaseDataContext` — without it the admin renders rows with only their
|
|
3001
|
-
* `id`.
|
|
3002
|
-
*/
|
|
3003
|
-
function wrapAsEntityData(sdkData) {
|
|
3004
|
-
const cache = /* @__PURE__ */ new Map();
|
|
3005
|
-
function getAccessor(slug) {
|
|
3006
|
-
let accessor = cache.get(slug);
|
|
3007
|
-
if (!accessor) {
|
|
3008
|
-
accessor = toEntityAccessor(sdkData.collection(slug), slug);
|
|
3009
|
-
cache.set(slug, accessor);
|
|
3010
|
-
}
|
|
3011
|
-
return accessor;
|
|
3012
|
-
}
|
|
3013
|
-
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
3014
|
-
if (prop === "collection") return getAccessor;
|
|
3015
|
-
if (typeof prop === "symbol") return void 0;
|
|
3016
|
-
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
3017
|
-
return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
|
|
3018
|
-
} });
|
|
3019
|
-
}
|
|
3020
|
-
/**
|
|
3021
|
-
* Wrap a Entity-shaped {@link RebaseData} into a flat {@link RebaseSdkData}.
|
|
3022
|
-
*
|
|
3023
|
-
* Every collection accessor is adapted to return flat rows. Use this to derive
|
|
3024
|
-
* the flat SDK data layer (`context.data`) from an existing Entity data layer
|
|
3025
|
-
* — e.g. the admin routes its Entity data via `useData()` and exposes the
|
|
3026
|
-
* same routing as flat `context.data` for callbacks by wrapping it here.
|
|
3027
|
-
*/
|
|
3028
|
-
function wrapAsSdkData(entityData) {
|
|
3029
|
-
const cache = /* @__PURE__ */ new Map();
|
|
3030
|
-
function getAccessor(slug) {
|
|
3031
|
-
let accessor = cache.get(slug);
|
|
3032
|
-
if (!accessor) {
|
|
3033
|
-
accessor = toSdkCollectionClient(entityData.collection(slug));
|
|
3034
|
-
cache.set(slug, accessor);
|
|
3035
|
-
}
|
|
3036
|
-
return accessor;
|
|
3037
|
-
}
|
|
3038
|
-
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
3039
|
-
if (prop === "collection") return getAccessor;
|
|
3040
|
-
if (typeof prop === "symbol") return void 0;
|
|
3041
|
-
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
3042
|
-
return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
|
|
3043
|
-
} });
|
|
3044
|
-
}
|
|
3045
|
-
/**
|
|
3046
|
-
* Build a flat {@link RebaseSdkData} from a `DataDriver`.
|
|
3047
|
-
*
|
|
3048
|
-
* This is the developer-facing SDK data layer used by backend framework
|
|
3049
|
-
* callbacks & scripts (`context.data` / `rebase.data`). It returns flat rows —
|
|
3050
|
-
* identical in shape to the frontend SDK client — so the API is symmetric
|
|
3051
|
-
* across front and back. The admin CMS uses {@link buildRebaseData} (Entity).
|
|
3052
|
-
*/
|
|
3053
|
-
function buildSdkData(driver) {
|
|
3054
|
-
return wrapAsSdkData(buildRebaseData(driver));
|
|
3055
|
-
}
|
|
3056
|
-
//#endregion
|
|
3057
|
-
//#region src/data/buildRoutedRebaseData.ts
|
|
3058
|
-
/**
|
|
3059
|
-
* Build a {@link RebaseData} that routes each collection to the right
|
|
3060
|
-
* backend based on its resolved data source.
|
|
3061
|
-
*
|
|
3062
|
-
* `.collection(path)` (and dynamic `data.products`-style access) resolves the
|
|
3063
|
-
* collection's data-source key via `resolveKey` and delegates to the matching
|
|
3064
|
-
* entry in `sources`, falling back to `defaultData` when there is no match.
|
|
3065
|
-
* Because routing keys off the *path being accessed*, a reference widget
|
|
3066
|
-
* inside a Firestore form that points at a Postgres collection is still
|
|
3067
|
-
* served by Postgres — routing follows the target, not the ancestor.
|
|
3068
|
-
*
|
|
3069
|
-
* When `sources` is empty this returns `defaultData` untouched, so the
|
|
3070
|
-
* single-driver setup keeps identical behaviour and identity (important for
|
|
3071
|
-
* effect dependencies that key off the data instance).
|
|
3072
|
-
*
|
|
3073
|
-
* @example
|
|
3074
|
-
* const data = buildRoutedRebaseData({
|
|
3075
|
-
* defaultData: client.data,
|
|
3076
|
-
* sources: { analytics: buildRebaseData(firestoreDriver) },
|
|
3077
|
-
* resolveKey: (path) => resolveDataSource(registry.getCollection(path), defs).key
|
|
3078
|
-
* });
|
|
3079
|
-
* await data.products.find(); // → default (server / Postgres)
|
|
3080
|
-
* await data.events.find(); // → Firestore, if `events.dataSource === "analytics"`
|
|
3081
|
-
*/
|
|
3082
|
-
function buildRoutedRebaseData({ defaultData, sources, resolveKey }) {
|
|
3083
|
-
if (!sources || Object.keys(sources).length === 0) return defaultData;
|
|
3084
|
-
function resolve(slugOrPath) {
|
|
3085
|
-
const key = resolveKey(slugOrPath);
|
|
3086
|
-
if (key && sources[key]) return sources[key];
|
|
3087
|
-
return defaultData;
|
|
3088
|
-
}
|
|
3089
|
-
function getAccessor(slugOrPath) {
|
|
3090
|
-
return resolve(slugOrPath).collection(slugOrPath);
|
|
3091
|
-
}
|
|
3092
|
-
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
3093
|
-
if (prop === "collection") return getAccessor;
|
|
3094
|
-
if (typeof prop === "symbol") return void 0;
|
|
3095
|
-
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
3096
|
-
return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
|
|
3097
|
-
} });
|
|
3098
|
-
}
|
|
3099
|
-
//#endregion
|
|
3100
|
-
//#region src/data/sort-dialect.ts
|
|
3101
|
-
/**
|
|
3102
|
-
* Sort-order wire codec.
|
|
3103
|
-
*
|
|
3104
|
-
* This is the ONLY module that knows about the colon-delimited wire format
|
|
3105
|
-
* (`"field:direction"`) used in HTTP query parameters.
|
|
3106
|
-
* Everything else speaks {@link OrderByTuple} exclusively.
|
|
3107
|
-
*
|
|
3108
|
-
* Mirrors the filter architecture in `filter-dialect.ts`.
|
|
3109
|
-
*
|
|
3110
|
-
* @module
|
|
3111
|
-
*/
|
|
3112
|
-
/**
|
|
3113
|
-
* Serialize an {@link OrderByTuple} to the wire format `"field:direction"`.
|
|
3114
|
-
*
|
|
3115
|
-
* **Runtime tolerance:** if the input is already a well-formed wire string
|
|
3116
|
-
* (from an untyped JS caller), it is returned unchanged.
|
|
3117
|
-
* This is undocumented tolerance, not public API — don't rely on it.
|
|
3118
|
-
*
|
|
3119
|
-
* @param orderBy - A canonical `[field, direction]` tuple, or at runtime
|
|
3120
|
-
* possibly a pre-serialized string (undocumented tolerance).
|
|
3121
|
-
* @returns The wire-format string, or `undefined` if the input is falsy.
|
|
3122
|
-
*
|
|
3123
|
-
* @remarks
|
|
3124
|
-
* Field names containing `:` are representable in the tuple form but
|
|
3125
|
-
* **not** on the wire — this is an inherent limitation of the colon-delimited
|
|
3126
|
-
* encoding and is not resolved here.
|
|
3127
|
-
*/
|
|
3128
|
-
function serializeOrderBy(orderBy) {
|
|
3129
|
-
if (!orderBy) return void 0;
|
|
3130
|
-
if (typeof orderBy === "string") return orderBy;
|
|
3131
|
-
return `${orderBy[0]}:${orderBy[1]}`;
|
|
3132
|
-
}
|
|
3133
|
-
/**
|
|
3134
|
-
* Deserialize a wire-format `"field:direction"` string into an {@link OrderByTuple}.
|
|
3135
|
-
*
|
|
3136
|
-
* Lenient parsing (matches existing server behaviour):
|
|
3137
|
-
* - Bare field name (no colon): `"name"` → `["name", "asc"]`
|
|
3138
|
-
* - Unknown direction: `"name:foo"` → `["name", "asc"]`
|
|
3139
|
-
* - Empty / falsy input: → `undefined`
|
|
3140
|
-
*
|
|
3141
|
-
* @param raw - The wire-format string from an HTTP query parameter.
|
|
3142
|
-
* @returns The canonical tuple, or `undefined` if the input is empty/falsy.
|
|
3143
|
-
*/
|
|
3144
|
-
function deserializeOrderBy(raw) {
|
|
3145
|
-
if (!raw) return void 0;
|
|
3146
|
-
const idx = raw.indexOf(":");
|
|
3147
|
-
if (idx === -1) return [raw, "asc"];
|
|
3148
|
-
return [raw.slice(0, idx), raw.slice(idx + 1) === "desc" ? "desc" : "asc"];
|
|
3149
|
-
}
|
|
3150
|
-
//#endregion
|
|
3151
|
-
//#region src/table-classification.ts
|
|
3152
|
-
/** Schemas that are always considered Rebase-internal. */
|
|
3153
|
-
var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
|
|
3154
|
-
/** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
|
|
3155
|
-
var REBASE_INTERNAL_PREFIXES = [
|
|
3156
|
-
"_rebase_",
|
|
3157
|
-
"_auth_",
|
|
3158
|
-
"drizzle_"
|
|
3159
|
-
];
|
|
3160
|
-
/**
|
|
3161
|
-
* Synchronously classify a table based on naming conventions.
|
|
3162
|
-
*
|
|
3163
|
-
* @param tableName - The unqualified name of the table.
|
|
3164
|
-
* @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
|
|
3165
|
-
* @returns `"rebase-internal"` when the table belongs to a reserved schema or
|
|
3166
|
-
* carries a reserved prefix; `"user"` otherwise.
|
|
3167
|
-
*
|
|
3168
|
-
* @remarks
|
|
3169
|
-
* Junction-table detection requires an async database query and is therefore
|
|
3170
|
-
* **not** handled by this function. Use {@link detectJunctionTables} to obtain
|
|
3171
|
-
* the set of junction tables, then reclassify as needed.
|
|
3172
|
-
*/
|
|
3173
|
-
function classifyTable(tableName, schemaName) {
|
|
3174
|
-
if (REBASE_INTERNAL_SCHEMAS.includes(schemaName) || REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))) return "rebase-internal";
|
|
3175
|
-
return "user";
|
|
3176
|
-
}
|
|
3177
|
-
/**
|
|
3178
|
-
* Convenience predicate that checks whether a table is Rebase-internal.
|
|
3179
|
-
*
|
|
3180
|
-
* @param tableName - The unqualified name of the table.
|
|
3181
|
-
* @param schemaName - The schema the table belongs to.
|
|
3182
|
-
* @returns `true` if the table is classified as `"rebase-internal"`.
|
|
3183
|
-
*/
|
|
3184
|
-
function isRebaseInternalTable(tableName, schemaName) {
|
|
3185
|
-
return classifyTable(tableName, schemaName) === "rebase-internal";
|
|
3186
|
-
}
|
|
3187
|
-
/** SQL query that detects junction tables in the `public` schema. */
|
|
3188
|
-
var JUNCTION_TABLES_SQL = `
|
|
3189
|
-
SELECT t.table_name
|
|
3190
|
-
FROM information_schema.tables t
|
|
3191
|
-
WHERE t.table_schema = 'public'
|
|
3192
|
-
AND t.table_type = 'BASE TABLE'
|
|
3193
|
-
AND NOT EXISTS (
|
|
3194
|
-
SELECT 1
|
|
3195
|
-
FROM information_schema.columns c
|
|
3196
|
-
WHERE c.table_schema = t.table_schema
|
|
3197
|
-
AND c.table_name = t.table_name
|
|
3198
|
-
AND c.column_name NOT IN (
|
|
3199
|
-
SELECT kcu.column_name
|
|
3200
|
-
FROM information_schema.key_column_usage kcu
|
|
3201
|
-
JOIN information_schema.table_constraints tc
|
|
3202
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
3203
|
-
AND tc.table_schema = kcu.table_schema
|
|
3204
|
-
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
3205
|
-
AND kcu.table_schema = t.table_schema
|
|
3206
|
-
AND kcu.table_name = t.table_name
|
|
3207
|
-
)
|
|
3208
|
-
)
|
|
3209
|
-
`;
|
|
3210
|
-
/**
|
|
3211
|
-
* Asynchronously detect junction (link) tables in the `public` schema.
|
|
3212
|
-
*
|
|
3213
|
-
* A junction table is defined as a table where **every** column participates in
|
|
3214
|
-
* at least one foreign-key constraint.
|
|
3215
|
-
*
|
|
3216
|
-
* @param executeSql - A callback that executes a raw SQL string and returns the
|
|
3217
|
-
* resulting rows.
|
|
3218
|
-
* @returns A `Set` containing the names of all detected junction tables.
|
|
3219
|
-
*/
|
|
3220
|
-
async function detectJunctionTables(executeSql) {
|
|
3221
|
-
const rows = await executeSql(JUNCTION_TABLES_SQL);
|
|
3222
|
-
const junctionTables = /* @__PURE__ */ new Set();
|
|
3223
|
-
for (const row of rows) if (typeof row.table_name === "string") junctionTables.add(row.table_name);
|
|
3224
|
-
return junctionTables;
|
|
3225
|
-
}
|
|
3226
|
-
//#endregion
|
|
3227
|
-
exports.COLLECTION_PATH_SEPARATOR = COLLECTION_PATH_SEPARATOR;
|
|
3228
|
-
exports.CollectionRegistry = CollectionRegistry;
|
|
3229
|
-
exports.DEFAULT_ONE_OF_TYPE = DEFAULT_ONE_OF_TYPE;
|
|
3230
|
-
exports.DEFAULT_ONE_OF_VALUE = DEFAULT_ONE_OF_VALUE;
|
|
3231
|
-
exports.JUNCTION_TABLES_SQL = JUNCTION_TABLES_SQL;
|
|
3232
|
-
exports.QueryBuilder = QueryBuilder;
|
|
3233
|
-
exports.REBASE_INTERNAL_PREFIXES = REBASE_INTERNAL_PREFIXES;
|
|
3234
|
-
exports.REBASE_INTERNAL_SCHEMAS = REBASE_INTERNAL_SCHEMAS;
|
|
3235
|
-
exports.addInitialSlash = addInitialSlash;
|
|
3236
|
-
exports.and = and;
|
|
3237
|
-
exports.applyPropertyConditions = applyPropertyConditions;
|
|
3238
|
-
exports.buildCollection = buildCollection;
|
|
3239
|
-
exports.buildConditionContext = buildConditionContext;
|
|
3240
|
-
exports.buildProperty = buildProperty;
|
|
3241
|
-
exports.buildPropertyCallbacks = buildPropertyCallbacks;
|
|
3242
|
-
exports.buildRebaseData = buildRebaseData;
|
|
3243
|
-
exports.buildRoutedRebaseData = buildRoutedRebaseData;
|
|
3244
|
-
exports.buildSdkData = buildSdkData;
|
|
3245
|
-
exports.canCreateEntity = canCreateEntity;
|
|
3246
|
-
exports.canDeleteEntity = canDeleteEntity;
|
|
3247
|
-
exports.canEditEntity = canEditEntity;
|
|
3248
|
-
exports.canReadCollection = canReadCollection;
|
|
3249
|
-
exports.checkOperation = checkOperation;
|
|
3250
|
-
exports.classifyTable = classifyTable;
|
|
3251
|
-
exports.cond = cond;
|
|
3252
|
-
exports.createDataSourceRegistry = createDataSourceRegistry;
|
|
3253
|
-
exports.createRelationRef = createRelationRef;
|
|
3254
|
-
exports.createRelationRefWithData = createRelationRefWithData;
|
|
3255
|
-
exports.defaultUsersCollection = defaultUsersCollection;
|
|
3256
|
-
exports.defineCollection = defineCollection;
|
|
3257
|
-
exports.deserializeFilter = deserializeFilter;
|
|
3258
|
-
exports.deserializeLogicalCondition = deserializeLogicalCondition;
|
|
3259
|
-
exports.deserializeOrderBy = deserializeOrderBy;
|
|
3260
|
-
exports.detectJunctionTables = detectJunctionTables;
|
|
3261
|
-
exports.enumToObjectEntries = enumToObjectEntries;
|
|
3262
|
-
exports.evaluateCondition = evaluateCondition;
|
|
3263
|
-
exports.evaluatePolicy = evaluatePolicy;
|
|
3264
|
-
exports.findRelation = findRelation;
|
|
3265
|
-
exports.fullPathToCollectionSegments = fullPathToCollectionSegments;
|
|
3266
|
-
exports.getArrayResolvedProperties = getArrayResolvedProperties;
|
|
3267
|
-
exports.getCollectionBySlugWithin = getCollectionBySlugWithin;
|
|
3268
|
-
exports.getCollectionPathsCombinations = getCollectionPathsCombinations;
|
|
3269
|
-
exports.getColumnName = getColumnName;
|
|
3270
|
-
exports.getDefaultValueFor = getDefaultValueFor;
|
|
3271
|
-
exports.getDefaultValueFortype = getDefaultValueFortype;
|
|
3272
|
-
exports.getDefaultValuesFor = getDefaultValuesFor;
|
|
3273
|
-
exports.getEntityImagePreviewPropertyKey = getEntityImagePreviewPropertyKey;
|
|
3274
|
-
exports.getEnumVarName = getEnumVarName;
|
|
3275
|
-
exports.getLabelOrConfigFrom = getLabelOrConfigFrom;
|
|
3276
|
-
exports.getLastSegment = getLastSegment;
|
|
3277
|
-
exports.getLocalChangesBackup = getLocalChangesBackup;
|
|
3278
|
-
exports.getNavigationEntriesFromPath = getNavigationEntriesFromPath;
|
|
3279
|
-
exports.getParentReferencesFromPath = getParentReferencesFromPath;
|
|
3280
|
-
exports.getPrimaryKeys = getPrimaryKeys;
|
|
3281
|
-
exports.getReferenceFrom = getReferenceFrom;
|
|
3282
|
-
exports.getRelationFrom = getRelationFrom;
|
|
3283
|
-
exports.getSubcollections = getSubcollections;
|
|
3284
|
-
exports.getTableName = getTableName;
|
|
3285
|
-
exports.getTableVarName = getTableVarName;
|
|
3286
|
-
exports.isHidden = isHidden;
|
|
3287
|
-
exports.isPropertyBuilder = isPropertyBuilder;
|
|
3288
|
-
exports.isReadOnly = isReadOnly;
|
|
3289
|
-
exports.isRebaseInternalTable = isRebaseInternalTable;
|
|
3290
|
-
exports.normalizeToEntityRelation = normalizeToEntityRelation;
|
|
3291
|
-
exports.or = or;
|
|
3292
|
-
exports.policyToPostgres = policyToPostgres;
|
|
3293
|
-
exports.registerConditionOperations = registerConditionOperations;
|
|
3294
|
-
exports.removeInitialAndTrailingSlashes = removeInitialAndTrailingSlashes;
|
|
3295
|
-
exports.removeInitialSlash = removeInitialSlash;
|
|
3296
|
-
exports.removeTrailingSlash = removeTrailingSlash;
|
|
3297
|
-
exports.resolveArrayProperties = resolveArrayProperties;
|
|
3298
|
-
exports.resolveCollectionPathIds = resolveCollectionPathIds;
|
|
3299
|
-
exports.resolveCollectionRelations = resolveCollectionRelations;
|
|
3300
|
-
exports.resolveDataSource = resolveDataSource;
|
|
3301
|
-
exports.resolveDefaultSelectedView = resolveDefaultSelectedView;
|
|
3302
|
-
exports.resolveEnumValues = resolveEnumValues;
|
|
3303
|
-
exports.resolveFilterOperators = resolveFilterOperators;
|
|
3304
|
-
exports.resolveProperties = resolveProperties;
|
|
3305
|
-
exports.resolveProperty = resolveProperty;
|
|
3306
|
-
exports.resolvePropertyEnum = resolvePropertyEnum;
|
|
3307
|
-
exports.resolvePropertyRelation = resolvePropertyRelation;
|
|
3308
|
-
exports.resolveRelationProperty = resolveRelationProperty;
|
|
3309
|
-
exports.resolveStorageFilenameString = resolveStorageFilenameString;
|
|
3310
|
-
exports.resolveStoragePathString = resolveStoragePathString;
|
|
3311
|
-
exports.resolveStorageSource = resolveStorageSource;
|
|
3312
|
-
exports.sanitizeData = sanitizeData;
|
|
3313
|
-
exports.sanitizeRelation = sanitizeRelation;
|
|
3314
|
-
exports.securityRuleToConditions = securityRuleToConditions;
|
|
3315
|
-
exports.segmentsToStrippedPath = segmentsToStrippedPath;
|
|
3316
|
-
exports.serializeFilter = serializeFilter;
|
|
3317
|
-
exports.serializeLogicalCondition = serializeLogicalCondition;
|
|
3318
|
-
exports.serializeOrderBy = serializeOrderBy;
|
|
3319
|
-
exports.sortProperties = sortProperties;
|
|
3320
|
-
exports.stripCollectionPath = stripCollectionPath;
|
|
3321
|
-
exports.traverseValueProperty = traverseValueProperty;
|
|
3322
|
-
exports.traverseValuesProperties = traverseValuesProperties;
|
|
3323
|
-
exports.updateDateAutoValues = updateDateAutoValues;
|
|
3324
|
-
exports.wrapAsEntityData = wrapAsEntityData;
|
|
3325
|
-
exports.wrapAsSdkData = wrapAsSdkData;
|
|
3326
|
-
});
|
|
3327
|
-
|
|
3328
|
-
//# sourceMappingURL=index.umd.js.map
|