@rebasepro/common 0.24.0 → 0.24.1-canary.g56af4a4
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/CollectionRegistry.es.js +404 -0
- package/dist/collections/CollectionRegistry.es.js.map +1 -0
- package/dist/collections/auth-secrets.es.js +47 -0
- package/dist/collections/auth-secrets.es.js.map +1 -0
- package/dist/collections/default-collections.es.js +120 -0
- package/dist/collections/default-collections.es.js.map +1 -0
- package/dist/collections/engine-support.es.js +35 -0
- package/dist/collections/engine-support.es.js.map +1 -0
- package/dist/collections/field-access.d.ts +13 -0
- package/dist/collections/field-access.es.js +142 -0
- package/dist/collections/field-access.es.js.map +1 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/soft-delete.es.js +34 -0
- package/dist/collections/soft-delete.es.js.map +1 -0
- package/dist/collections/unknown-field.d.ts +65 -0
- package/dist/collections/unknown-field.es.js +135 -0
- package/dist/collections/unknown-field.es.js.map +1 -0
- package/dist/data/buildRebaseData.d.ts +12 -2
- package/dist/data/buildRebaseData.es.js +1026 -0
- package/dist/data/buildRebaseData.es.js.map +1 -0
- package/dist/data/buildRoutedRebaseData.d.ts +6 -1
- package/dist/data/buildRoutedRebaseData.es.js +47 -0
- package/dist/data/buildRoutedRebaseData.es.js.map +1 -0
- package/dist/data/collection-accessor.d.ts +54 -0
- package/dist/data/collection-accessor.es.js +101 -0
- package/dist/data/collection-accessor.es.js.map +1 -0
- package/dist/data/cursor.es.js +166 -0
- package/dist/data/cursor.es.js.map +1 -0
- package/dist/data/filter-conditions.es.js +24 -0
- package/dist/data/filter-conditions.es.js.map +1 -0
- package/dist/data/filter-dialect.d.ts +3 -3
- package/dist/data/filter-dialect.es.js +786 -0
- package/dist/data/filter-dialect.es.js.map +1 -0
- package/dist/data/include-spec.d.ts +2 -0
- package/dist/data/include-spec.es.js +273 -0
- package/dist/data/include-spec.es.js.map +1 -0
- package/dist/data/paginate.d.ts +8 -1
- package/dist/data/paginate.es.js +181 -0
- package/dist/data/paginate.es.js.map +1 -0
- package/dist/data/query_builder.d.ts +52 -4
- package/dist/data/query_builder.es.js +151 -0
- package/dist/data/query_builder.es.js.map +1 -0
- package/dist/data/resolveDataSource.es.js +87 -0
- package/dist/data/resolveDataSource.es.js.map +1 -0
- package/dist/data/row-etag.d.ts +46 -0
- package/dist/data/row-etag.es.js +64 -0
- package/dist/data/row-etag.es.js.map +1 -0
- package/dist/data/sort-dialect.es.js +236 -0
- package/dist/data/sort-dialect.es.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +56 -7588
- package/dist/table-classification.es.js +107 -0
- package/dist/table-classification.es.js.map +1 -0
- package/dist/util/auth-default-policies.es.js +184 -0
- package/dist/util/auth-default-policies.es.js.map +1 -0
- package/dist/util/builders.es.js +15 -0
- package/dist/util/builders.es.js.map +1 -0
- package/dist/util/callback-errors.es.js +164 -0
- package/dist/util/callback-errors.es.js.map +1 -0
- package/dist/util/callbacks.es.js +72 -0
- package/dist/util/callbacks.es.js.map +1 -0
- package/dist/util/collections.es.js +79 -0
- package/dist/util/collections.es.js.map +1 -0
- package/dist/util/common.es.js +7 -0
- package/dist/util/common.es.js.map +1 -0
- package/dist/util/conditions.es.js +100 -0
- package/dist/util/conditions.es.js.map +1 -0
- package/dist/util/copy.es.js +74 -0
- package/dist/util/copy.es.js.map +1 -0
- package/dist/util/download-url-cache.d.ts +122 -0
- package/dist/util/download-url-cache.es.js +187 -0
- package/dist/util/download-url-cache.es.js.map +1 -0
- package/dist/util/email.es.js +36 -0
- package/dist/util/email.es.js.map +1 -0
- package/dist/util/entities.d.ts +18 -1
- package/dist/util/entities.es.js +328 -0
- package/dist/util/entities.es.js.map +1 -0
- package/dist/util/enums.d.ts +10 -1
- package/dist/util/enums.es.js +37 -0
- package/dist/util/enums.es.js.map +1 -0
- package/dist/util/identity.es.js +138 -0
- package/dist/util/identity.es.js.map +1 -0
- package/dist/util/index.d.ts +4 -0
- package/dist/util/internal-tables.d.ts +10 -0
- package/dist/util/internal-tables.es.js +202 -0
- package/dist/util/internal-tables.es.js.map +1 -0
- package/dist/util/junction-policies.es.js +273 -0
- package/dist/util/junction-policies.es.js.map +1 -0
- package/dist/util/paths.es.js +26 -0
- package/dist/util/paths.es.js.map +1 -0
- package/dist/util/permissions.es.js +108 -0
- package/dist/util/permissions.es.js.map +1 -0
- package/dist/util/pg-column-to-property.es.js +303 -0
- package/dist/util/pg-column-to-property.es.js.map +1 -0
- package/dist/util/policy/compileRulePolicies.es.js +43 -0
- package/dist/util/policy/compileRulePolicies.es.js.map +1 -0
- package/dist/util/policy/evaluatePolicy.es.js +104 -0
- package/dist/util/policy/evaluatePolicy.es.js.map +1 -0
- package/dist/util/policy/policyToPostgres.d.ts +24 -0
- package/dist/util/policy/policyToPostgres.es.js +593 -0
- package/dist/util/policy/policyToPostgres.es.js.map +1 -0
- package/dist/util/policy/policyToSecurityRule.es.js +79 -0
- package/dist/util/policy/policyToSecurityRule.es.js.map +1 -0
- package/dist/util/policy/securityRuleToConditions.es.js +48 -0
- package/dist/util/policy/securityRuleToConditions.es.js.map +1 -0
- package/dist/util/policy/sqlToPolicy.es.js +326 -0
- package/dist/util/policy/sqlToPolicy.es.js.map +1 -0
- package/dist/util/relations.es.js +243 -0
- package/dist/util/relations.es.js.map +1 -0
- package/dist/util/resolutions.es.js +318 -0
- package/dist/util/resolutions.es.js.map +1 -0
- package/dist/util/resolve-relation.es.js +163 -0
- package/dist/util/resolve-relation.es.js.map +1 -0
- package/dist/util/resumable-upload-threshold.d.ts +15 -0
- package/dist/util/resumable-upload-threshold.es.js +20 -0
- package/dist/util/resumable-upload-threshold.es.js.map +1 -0
- package/dist/util/resumable-upload.d.ts +51 -0
- package/dist/util/resumable-upload.es.js +121 -0
- package/dist/util/resumable-upload.es.js.map +1 -0
- package/dist/util/sql-redaction.d.ts +21 -0
- package/dist/util/sql-redaction.es.js +107 -0
- package/dist/util/sql-redaction.es.js.map +1 -0
- package/dist/util/sql-rows.es.js +41 -0
- package/dist/util/sql-rows.es.js.map +1 -0
- package/dist/util/storage.d.ts +26 -4
- package/dist/util/storage.es.js +117 -0
- package/dist/util/storage.es.js.map +1 -0
- package/dist/util/string-column-length.es.js +31 -0
- package/dist/util/string-column-length.es.js.map +1 -0
- package/dist/util/tenant.es.js +201 -0
- package/dist/util/tenant.es.js.map +1 -0
- package/dist/util/untrusted-envelope.es.js +40 -0
- package/dist/util/untrusted-envelope.es.js.map +1 -0
- package/package.json +5 -3
- package/dist/index.es.js.map +0 -1
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
import { buildCompositeId, resolvePrimaryKeys } from "../util/identity.es.js";
|
|
2
|
+
import { resolveCollectionRelations } from "../util/relations.es.js";
|
|
3
|
+
import { cursorToStartAfter, decodeCursor, reconcileCursorOrder } from "./cursor.es.js";
|
|
4
|
+
import { normalizeOrderBy } from "./sort-dialect.es.js";
|
|
5
|
+
import { mergeIncludeSpecs } from "./include-spec.es.js";
|
|
6
|
+
import { etagOf } from "./row-etag.es.js";
|
|
7
|
+
import { QueryBuilder } from "./query_builder.es.js";
|
|
8
|
+
import { collectAllPages, paginateFind, resolveFindWindow } from "./paginate.es.js";
|
|
9
|
+
import { deserializeFilter } from "./filter-dialect.es.js";
|
|
10
|
+
import { DEFAULT_VECTOR_LIST_LIMIT, EntityRelation, RebaseApiError, RebaseClientError, isUnsupported, sortKeyToString, unsupportedMethod } from "@rebasepro/types";
|
|
11
|
+
import { toSnakeCase, toWireKey } from "@rebasepro/utils";
|
|
12
|
+
//#region src/data/buildRebaseData.ts
|
|
13
|
+
/**
|
|
14
|
+
* What a client says when its data source cannot subscribe.
|
|
15
|
+
*
|
|
16
|
+
* Named rather than inlined so the sentence a caller sees does not depend on
|
|
17
|
+
* which of the two adapters below happened to build the client.
|
|
18
|
+
*/
|
|
19
|
+
var noRealtime = (slug) => `Realtime is not available for "${slug}": its data source does not support subscriptions.`;
|
|
20
|
+
/** What a client says when its data source cannot count. */
|
|
21
|
+
var noCount = (slug) => `Counting is not available for "${slug}": its data source does not support it.`;
|
|
22
|
+
/**
|
|
23
|
+
* Derive the response key an aggregate comes back under.
|
|
24
|
+
*
|
|
25
|
+
* `sum(total)` → `sum_total`, `count()` → `count`. Written once, here, because
|
|
26
|
+
* the REST parser derives the same alias from `?select=sum(total)` and the two
|
|
27
|
+
* have to agree — a caller reading `row.sum_total` off an SDK result and off an
|
|
28
|
+
* HTTP response is reading the same key or the SDK is broken.
|
|
29
|
+
*/
|
|
30
|
+
function aggregateAlias(fn, field) {
|
|
31
|
+
return field ? `${fn}_${field}` : fn;
|
|
32
|
+
}
|
|
33
|
+
function toDriverAggregate(select) {
|
|
34
|
+
const field = select.field;
|
|
35
|
+
return {
|
|
36
|
+
fn: select.fn,
|
|
37
|
+
field,
|
|
38
|
+
alias: aggregateAlias(select.fn, field)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* What a client says when its data source cannot aggregate.
|
|
43
|
+
*
|
|
44
|
+
* A stub rather than a fallback that fetches and reduces in JavaScript: that
|
|
45
|
+
* would be wrong under a `limit` and unaffordable without one, and it would look
|
|
46
|
+
* like it had worked.
|
|
47
|
+
*/
|
|
48
|
+
/** What a client says when its data source cannot read a collection's trash. */
|
|
49
|
+
var noTrash = (slug) => `The trash is not available for "${slug}": its data source cannot read soft-deleted rows.`;
|
|
50
|
+
/** What `softDelete: true` names the field a delete stamps; a restore clears it. */
|
|
51
|
+
var DEFAULT_SOFT_DELETE_FIELD = "deletedAt";
|
|
52
|
+
var noAggregate = (slug) => `Aggregates are not available for "${slug}": its data source does not implement them.`;
|
|
53
|
+
function createPrimaryKeyResolver(options) {
|
|
54
|
+
const cache = /* @__PURE__ */ new Map();
|
|
55
|
+
const warned = /* @__PURE__ */ new Set();
|
|
56
|
+
return function primaryKeysFor(slug) {
|
|
57
|
+
const cached = cache.get(slug);
|
|
58
|
+
if (cached) return cached;
|
|
59
|
+
const collection = options?.resolveCollection?.(slug);
|
|
60
|
+
if (!collection) return [];
|
|
61
|
+
const keys = resolvePrimaryKeys(collection);
|
|
62
|
+
if (keys.length > 0) {
|
|
63
|
+
cache.set(slug, keys);
|
|
64
|
+
return keys;
|
|
65
|
+
}
|
|
66
|
+
if (!warned.has(slug)) {
|
|
67
|
+
warned.add(slug);
|
|
68
|
+
console.warn(`[rebase] Collection '${slug}' declares no primary key, so its rows have no address: detail links, caching and relations will not work for it. Mark the key property with \`isId\` in its collection config — the server logs which column to mark at boot, if its schema knows the key.`);
|
|
69
|
+
}
|
|
70
|
+
return keys;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build the admin's view model out of the row the wire serves.
|
|
75
|
+
*
|
|
76
|
+
* The wire has ONE shape, for every consumer: flat columns, typed the way the
|
|
77
|
+
* database typed them, and a relation rendered as the target's own columns (or
|
|
78
|
+
* only its foreign key, when nothing asked for it). That is the REST contract,
|
|
79
|
+
* what `find()` returns, what `listen()` pushes, and what the generated types
|
|
80
|
+
* describe.
|
|
81
|
+
*
|
|
82
|
+
* The admin renders neither of those directly. Its date field requires a real
|
|
83
|
+
* `Date` and rejects a string outright; its relation cells read `.data.values`
|
|
84
|
+
* off a relation ref. Those requirements are the *admin's*, so they are met
|
|
85
|
+
* here — in the browser, from the collection config the panel already has —
|
|
86
|
+
* rather than by asking the server for a second wire shape.
|
|
87
|
+
*
|
|
88
|
+
* That second shape is what this replaces. Until 2026-09-09 the realtime wire
|
|
89
|
+
* carried the view model and every other read carried flat rows, so `find()`
|
|
90
|
+
* and `listen()` answered one query two ways; unifying the wire without doing
|
|
91
|
+
* this conversion is what left every date cell reading "Invalid date value"
|
|
92
|
+
* and every relation cell "Unexpected value".
|
|
93
|
+
*
|
|
94
|
+
* Values already in view-model form pass through untouched: a driver that
|
|
95
|
+
* still sends `{ __type: "date" }` or a relation ref (the client revives both)
|
|
96
|
+
* is served by the same walk.
|
|
97
|
+
*/
|
|
98
|
+
function toViewModelValues(values, properties, collection, resolveCollection) {
|
|
99
|
+
if (!properties) return values;
|
|
100
|
+
const relations = collection ? resolveCollectionRelations(collection) : {};
|
|
101
|
+
let out;
|
|
102
|
+
const write = (key, value) => {
|
|
103
|
+
out = out ?? { ...values };
|
|
104
|
+
out[key] = value;
|
|
105
|
+
};
|
|
106
|
+
for (const [key, rawProperty] of Object.entries(properties)) {
|
|
107
|
+
const property = rawProperty;
|
|
108
|
+
if (!property) continue;
|
|
109
|
+
if (!(key in values)) {
|
|
110
|
+
const fkRelation = relations[key];
|
|
111
|
+
const column = fkRelation && "localKey" in fkRelation ? fkRelation.localKey : void 0;
|
|
112
|
+
const fk = column !== void 0 ? values[column] ?? values[toWireKey(column)] : void 0;
|
|
113
|
+
const fkTarget = fkRelation?.targetSlug;
|
|
114
|
+
if (fkTarget && (typeof fk === "string" || typeof fk === "number")) write(key, new EntityRelation(fk, fkTarget));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const value = values[key];
|
|
118
|
+
if (value === null || value === void 0) continue;
|
|
119
|
+
const relation = relations[key];
|
|
120
|
+
if (relation && (property.type === "relation" || property.of?.type === "relation" || property.type === "array")) {
|
|
121
|
+
const target = relation.targetSlug;
|
|
122
|
+
if (!target) continue;
|
|
123
|
+
const targetProperties = resolveCollection?.(target)?.properties;
|
|
124
|
+
const targetCollection = resolveCollection?.(target);
|
|
125
|
+
const toRef = (item) => {
|
|
126
|
+
if (item instanceof EntityRelation) return item;
|
|
127
|
+
if (typeof item === "object" && item !== null && "__type" in item) return item;
|
|
128
|
+
if (typeof item === "object" && item !== null) {
|
|
129
|
+
const row = item;
|
|
130
|
+
const keys = targetCollection ? resolvePrimaryKeys(targetCollection) : [];
|
|
131
|
+
const id = keys.length > 0 ? buildCompositeId(row, keys) : row.id;
|
|
132
|
+
if (id === void 0 || id === null || id === "") return item;
|
|
133
|
+
return new EntityRelation(id, target, {
|
|
134
|
+
id,
|
|
135
|
+
path: target,
|
|
136
|
+
values: toViewModelValues(row, targetProperties, targetCollection, resolveCollection)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (typeof item === "string" || typeof item === "number") return new EntityRelation(item, target);
|
|
140
|
+
return item;
|
|
141
|
+
};
|
|
142
|
+
write(key, Array.isArray(value) ? value.map(toRef) : toRef(value));
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (property.type === "date" && !(value instanceof Date)) {
|
|
146
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
147
|
+
const date = new Date(value);
|
|
148
|
+
write(key, isNaN(date.getTime()) ? null : date);
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (property.type === "map" && property.properties && typeof value === "object" && !Array.isArray(value)) write(key, toViewModelValues(value, property.properties, void 0, resolveCollection));
|
|
153
|
+
}
|
|
154
|
+
return out ?? values;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Give a flat row the Entity view-model the admin renders.
|
|
158
|
+
*
|
|
159
|
+
* The address is *derived here* — it is not a column, and the row it came from
|
|
160
|
+
* does not contain one. Rows carry exactly what the table has, with the types
|
|
161
|
+
* Postgres returned; the id is this layer's invention, and this is the only
|
|
162
|
+
* place it is minted.
|
|
163
|
+
*
|
|
164
|
+
* `primaryKeys` empty falls back to a literal `id` on the row: drivers other
|
|
165
|
+
* than postgres still serve rows with one, and this keeps them working.
|
|
166
|
+
*/
|
|
167
|
+
function rowToEntity(row, slug, primaryKeys = [], toViewModel) {
|
|
168
|
+
const { _matches, ...values } = row;
|
|
169
|
+
const etag = etagOf(row);
|
|
170
|
+
return {
|
|
171
|
+
id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
|
|
172
|
+
path: slug,
|
|
173
|
+
values: toViewModel ? toViewModel(values) : values,
|
|
174
|
+
..._matches ? { searchMatches: _matches } : {},
|
|
175
|
+
...etag ? { etag } : {}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The relation envelope `toFlatRow` writes where a relation was:
|
|
180
|
+
* `{ id, path, __type: "relation", data: { id, path, values } }`. It is the
|
|
181
|
+
* admin's view-model, and the only pipeline that produces one is postgres'.
|
|
182
|
+
*/
|
|
183
|
+
function isRelationEnvelope(value) {
|
|
184
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && value.__type === "relation";
|
|
185
|
+
}
|
|
186
|
+
/** The target's own columns, as `toRestRow` would have inlined them. */
|
|
187
|
+
function inlineEnvelope(envelope) {
|
|
188
|
+
return envelope.data?.values ?? {};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Replace every relation envelope on a row with the target's flat columns.
|
|
192
|
+
*
|
|
193
|
+
* The SDK serves one relation shape — the inlined one (see
|
|
194
|
+
* {@link RestFetchService}) — and Postgres now serves it on every read, so
|
|
195
|
+
* against that driver this walk finds nothing to do. It stays for the drivers
|
|
196
|
+
* whose own `fetchCollection` still answers with refs: a developer reading
|
|
197
|
+
* through this accessor gets one shape whichever driver is underneath.
|
|
198
|
+
*
|
|
199
|
+
* Only applied where the REST pipeline is the contract (see `find`); a driver
|
|
200
|
+
* without a `restFetchService` keeps whatever it returns.
|
|
201
|
+
*
|
|
202
|
+
* Note this is NOT how the admin gets its view model — that is built in the
|
|
203
|
+
* browser by {@link toViewModelValues}, from the same flat row.
|
|
204
|
+
*/
|
|
205
|
+
function inlineRelationRefs(row) {
|
|
206
|
+
let out;
|
|
207
|
+
for (const [key, value] of Object.entries(row)) if (isRelationEnvelope(value)) {
|
|
208
|
+
out = out ?? { ...row };
|
|
209
|
+
out[key] = inlineEnvelope(value);
|
|
210
|
+
} else if (Array.isArray(value) && value.some(isRelationEnvelope)) {
|
|
211
|
+
out = out ?? { ...row };
|
|
212
|
+
out[key] = value.map((item) => isRelationEnvelope(item) ? inlineEnvelope(item) : item);
|
|
213
|
+
}
|
|
214
|
+
return out ?? row;
|
|
215
|
+
}
|
|
216
|
+
function createDriverAccessor(driver, slug, getPks = () => [], toViewModel, storedPath = () => slug) {
|
|
217
|
+
/**
|
|
218
|
+
* One page of the collection, and its count. `withDeleted: "only"` reads
|
|
219
|
+
* the trash (see `trash` below) through the same pipeline, so a trashed
|
|
220
|
+
* page is paged, counted and shaped exactly as a live one.
|
|
221
|
+
*/
|
|
222
|
+
async function findRows(params, withDeleted) {
|
|
223
|
+
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
224
|
+
const window = resolveFindWindow(params);
|
|
225
|
+
const { offset, driverOffset } = window;
|
|
226
|
+
const limit = params?.limit === void 0 && params?.vectorSearch ? DEFAULT_VECTOR_LIST_LIMIT : window.limit;
|
|
227
|
+
const cursor = params?.after ? decodeCursor(params.after) : void 0;
|
|
228
|
+
const sort = cursor ? reconcileCursorOrder(cursor, normalizeOrderBy(params?.orderBy)) : normalizeOrderBy(params?.orderBy);
|
|
229
|
+
const orderBy = sort && sort.length > 0 ? sort : void 0;
|
|
230
|
+
const startAfter = cursor ? cursorToStartAfter(cursor) : void 0;
|
|
231
|
+
const seeking = startAfter !== void 0;
|
|
232
|
+
const counting = driver.count !== void 0 && params?.count !== "none" && params?.distinct !== true && params?.vectorSearch?.threshold === void 0;
|
|
233
|
+
const probing = seeking || !counting;
|
|
234
|
+
const probeLimit = probing ? limit + 1 : limit;
|
|
235
|
+
const read = async (reader) => {
|
|
236
|
+
const fetchService = reader.restFetchService;
|
|
237
|
+
return {
|
|
238
|
+
fetched: fetchService ? await fetchService.fetchCollectionForRest(storedPath(), {
|
|
239
|
+
filter,
|
|
240
|
+
logical: params?.logical,
|
|
241
|
+
limit: probeLimit,
|
|
242
|
+
offset: seeking ? void 0 : driverOffset,
|
|
243
|
+
startAfter,
|
|
244
|
+
orderBy,
|
|
245
|
+
searchString: params?.searchString,
|
|
246
|
+
searchExplain: params?.searchExplain,
|
|
247
|
+
vectorSearch: params?.vectorSearch,
|
|
248
|
+
fields: params?.fields,
|
|
249
|
+
distinct: params?.distinct,
|
|
250
|
+
...withDeleted ? { withDeleted } : {}
|
|
251
|
+
}, params?.include) : await reader.fetchCollection({
|
|
252
|
+
path: storedPath(),
|
|
253
|
+
limit: probeLimit,
|
|
254
|
+
offset: seeking ? void 0 : driverOffset,
|
|
255
|
+
startAfter,
|
|
256
|
+
filter,
|
|
257
|
+
logical: params?.logical,
|
|
258
|
+
orderBy,
|
|
259
|
+
searchString: params?.searchString,
|
|
260
|
+
searchExplain: params?.searchExplain,
|
|
261
|
+
vectorSearch: params?.vectorSearch,
|
|
262
|
+
include: params?.include,
|
|
263
|
+
fields: params?.fields,
|
|
264
|
+
distinct: params?.distinct,
|
|
265
|
+
...withDeleted ? { withDeleted } : {}
|
|
266
|
+
}),
|
|
267
|
+
counted: counting && reader.count ? await reader.count({
|
|
268
|
+
path: storedPath(),
|
|
269
|
+
filter,
|
|
270
|
+
logical: params?.logical,
|
|
271
|
+
searchString: params?.searchString,
|
|
272
|
+
vectorSearch: params?.vectorSearch,
|
|
273
|
+
...withDeleted ? { withDeleted } : {}
|
|
274
|
+
}) : void 0
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
const { fetched, counted } = driver.readTogether ? await driver.readTogether(read) : await read(driver);
|
|
278
|
+
const rows = probing ? fetched.slice(0, limit) : fetched;
|
|
279
|
+
const total = counted ?? rows.length + offset;
|
|
280
|
+
const hasMore = probing ? fetched.length > limit : offset + rows.length < total;
|
|
281
|
+
const last = rows[rows.length - 1];
|
|
282
|
+
const nextCursor = hasMore && last && driver.restFetchService?.cursorFor ? driver.restFetchService.cursorFor(storedPath(), last, orderBy) : void 0;
|
|
283
|
+
return {
|
|
284
|
+
data: rows.map((row) => rowToEntity(row, slug, getPks(), toViewModel)),
|
|
285
|
+
meta: {
|
|
286
|
+
total,
|
|
287
|
+
limit,
|
|
288
|
+
offset,
|
|
289
|
+
hasMore,
|
|
290
|
+
...nextCursor && { nextCursor }
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async function countRows(params, withDeleted) {
|
|
295
|
+
if (!driver.count) throw new Error(noCount(slug));
|
|
296
|
+
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
297
|
+
return driver.count({
|
|
298
|
+
path: storedPath(),
|
|
299
|
+
filter,
|
|
300
|
+
logical: params?.logical,
|
|
301
|
+
searchString: params?.searchString,
|
|
302
|
+
vectorSearch: params?.vectorSearch,
|
|
303
|
+
...withDeleted ? { withDeleted } : {}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const accessor = {
|
|
307
|
+
find(params) {
|
|
308
|
+
return findRows(params);
|
|
309
|
+
},
|
|
310
|
+
async findById(id, options) {
|
|
311
|
+
const fetchService = driver.restFetchService;
|
|
312
|
+
const row = fetchService ? await fetchService.fetchOneForRest(storedPath(), id, options?.include) : await driver.fetchOne({
|
|
313
|
+
path: storedPath(),
|
|
314
|
+
id
|
|
315
|
+
});
|
|
316
|
+
return row ? rowToEntity(row, slug, getPks(), toViewModel) : void 0;
|
|
317
|
+
},
|
|
318
|
+
aggregate: driver.restFetchService?.aggregate ? async (params) => {
|
|
319
|
+
const limit = (params.groupBy?.length ?? 0) > 0 ? params.limit : void 0;
|
|
320
|
+
const rows = await driver.restFetchService.aggregate(storedPath(), {
|
|
321
|
+
aggregates: params.select.map(toDriverAggregate),
|
|
322
|
+
groupBy: params.groupBy,
|
|
323
|
+
filter: params.where ? deserializeFilter(params.where) : void 0,
|
|
324
|
+
logical: params.logical,
|
|
325
|
+
searchString: params.searchString,
|
|
326
|
+
limit: limit !== void 0 ? limit + 1 : params.limit,
|
|
327
|
+
offset: params.offset,
|
|
328
|
+
orderBy: normalizeOrderBy(params.orderBy)
|
|
329
|
+
});
|
|
330
|
+
if (limit === void 0) return rows;
|
|
331
|
+
const page = rows.slice(0, limit);
|
|
332
|
+
const meta = {
|
|
333
|
+
limit,
|
|
334
|
+
offset: params.offset ?? 0,
|
|
335
|
+
hasMore: rows.length > limit
|
|
336
|
+
};
|
|
337
|
+
Object.defineProperty(page, "meta", {
|
|
338
|
+
value: meta,
|
|
339
|
+
enumerable: false
|
|
340
|
+
});
|
|
341
|
+
return page;
|
|
342
|
+
} : void 0,
|
|
343
|
+
async create(data, id) {
|
|
344
|
+
return rowToEntity(await driver.save({
|
|
345
|
+
path: storedPath(),
|
|
346
|
+
values: data,
|
|
347
|
+
id,
|
|
348
|
+
status: "new"
|
|
349
|
+
}), slug, getPks(), toViewModel);
|
|
350
|
+
},
|
|
351
|
+
createMany: driver.saveMany ? async (data, options) => {
|
|
352
|
+
return (await driver.saveMany({
|
|
353
|
+
path: storedPath(),
|
|
354
|
+
rows: data,
|
|
355
|
+
upsert: options?.upsert,
|
|
356
|
+
onConflict: options?.onConflict
|
|
357
|
+
})).map((row) => rowToEntity(row, slug, getPks(), toViewModel));
|
|
358
|
+
} : void 0,
|
|
359
|
+
async update(id, data) {
|
|
360
|
+
return rowToEntity(await driver.save({
|
|
361
|
+
path: storedPath(),
|
|
362
|
+
values: data,
|
|
363
|
+
id,
|
|
364
|
+
status: "existing"
|
|
365
|
+
}), slug, getPks(), toViewModel);
|
|
366
|
+
},
|
|
367
|
+
async delete(id) {
|
|
368
|
+
return driver.delete({ row: {
|
|
369
|
+
id,
|
|
370
|
+
path: storedPath()
|
|
371
|
+
} });
|
|
372
|
+
},
|
|
373
|
+
updateMany: driver.updateMany ? async (updates) => {
|
|
374
|
+
return (await driver.updateMany({
|
|
375
|
+
path: storedPath(),
|
|
376
|
+
updates: updates.map((u) => ({
|
|
377
|
+
id: u.id,
|
|
378
|
+
values: u.data
|
|
379
|
+
}))
|
|
380
|
+
})).map((row) => rowToEntity(row, slug, getPks(), toViewModel));
|
|
381
|
+
} : void 0,
|
|
382
|
+
deleteMany: driver.deleteMany ? async (ids) => {
|
|
383
|
+
await driver.deleteMany({
|
|
384
|
+
path: storedPath(),
|
|
385
|
+
ids
|
|
386
|
+
});
|
|
387
|
+
} : void 0,
|
|
388
|
+
count: driver.count ? (params) => countRows(params) : void 0,
|
|
389
|
+
trash: {
|
|
390
|
+
find: (params) => findRows(params, "only"),
|
|
391
|
+
count: (params) => countRows(params, "only"),
|
|
392
|
+
purge: (id) => driver.delete({
|
|
393
|
+
row: {
|
|
394
|
+
id,
|
|
395
|
+
path: storedPath()
|
|
396
|
+
},
|
|
397
|
+
hard: true
|
|
398
|
+
}),
|
|
399
|
+
purgeMany: driver.deleteMany ? async (ids) => {
|
|
400
|
+
await driver.deleteMany({
|
|
401
|
+
path: storedPath(),
|
|
402
|
+
ids,
|
|
403
|
+
hard: true
|
|
404
|
+
});
|
|
405
|
+
} : void 0
|
|
406
|
+
},
|
|
407
|
+
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
408
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
409
|
+
const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
|
|
410
|
+
return driver.listenCollection({
|
|
411
|
+
path: storedPath(),
|
|
412
|
+
limit,
|
|
413
|
+
offset: driverOffset,
|
|
414
|
+
filter: params?.where,
|
|
415
|
+
logical: params?.logical,
|
|
416
|
+
orderBy: normalizeOrderBy(params?.orderBy),
|
|
417
|
+
searchString: params?.searchString,
|
|
418
|
+
searchExplain: params?.searchExplain,
|
|
419
|
+
vectorSearch: params?.vectorSearch,
|
|
420
|
+
onUpdate: (entities) => {
|
|
421
|
+
onUpdate({
|
|
422
|
+
data: entities.map((row) => rowToEntity(normalize(row), slug, getPks(), toViewModel)),
|
|
423
|
+
meta: {
|
|
424
|
+
total: offset + entities.length,
|
|
425
|
+
limit,
|
|
426
|
+
offset,
|
|
427
|
+
hasMore: entities.length >= limit
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
},
|
|
431
|
+
onError
|
|
432
|
+
});
|
|
433
|
+
} : void 0,
|
|
434
|
+
listenById: driver.listenOne ? (id, onUpdate, onError, options) => {
|
|
435
|
+
const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
|
|
436
|
+
return driver.listenOne({
|
|
437
|
+
path: storedPath(),
|
|
438
|
+
id,
|
|
439
|
+
include: options?.include,
|
|
440
|
+
onUpdate: (entity) => onUpdate(entity ? rowToEntity(normalize(entity), slug, getPks(), toViewModel) : void 0),
|
|
441
|
+
onError
|
|
442
|
+
});
|
|
443
|
+
} : void 0,
|
|
444
|
+
where(columnOrCondition, operator, value) {
|
|
445
|
+
const builder = new QueryBuilder(accessor);
|
|
446
|
+
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
447
|
+
return builder.where(columnOrCondition, operator, value);
|
|
448
|
+
},
|
|
449
|
+
orderBy(column, ascending) {
|
|
450
|
+
return new QueryBuilder(accessor).orderBy(column, ascending);
|
|
451
|
+
},
|
|
452
|
+
limit(count) {
|
|
453
|
+
return new QueryBuilder(accessor).limit(count);
|
|
454
|
+
},
|
|
455
|
+
offset(count) {
|
|
456
|
+
return new QueryBuilder(accessor).offset(count);
|
|
457
|
+
},
|
|
458
|
+
search(searchString, options) {
|
|
459
|
+
return new QueryBuilder(accessor).search(searchString, options);
|
|
460
|
+
},
|
|
461
|
+
vectorSearch(property, vector, options) {
|
|
462
|
+
return new QueryBuilder(accessor).vectorSearch(property, vector, options);
|
|
463
|
+
},
|
|
464
|
+
include(...relations) {
|
|
465
|
+
return new QueryBuilder(accessor).include(...relations);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
return accessor;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Build a `RebaseData` object from a `DataDriver` using JavaScript Proxy.
|
|
472
|
+
*
|
|
473
|
+
* This is the key bridge: any property access like `data.products` returns
|
|
474
|
+
* a `CollectionAccessor` backed by the underlying DataDriver, without
|
|
475
|
+
* needing per-collection code generation.
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* const data = buildRebaseData(driver);
|
|
479
|
+
* await data.products.create({ name: "Camera", price: 299 });
|
|
480
|
+
* const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
|
|
481
|
+
*/
|
|
482
|
+
/**
|
|
483
|
+
* The view-model converter for one collection, or `undefined` when there is no
|
|
484
|
+
* collection config to build it from.
|
|
485
|
+
*
|
|
486
|
+
* Absent is the honest answer for every consumer that is not the admin: the
|
|
487
|
+
* flat SDK derives itself from this same layer (`buildSdkData`) and must keep
|
|
488
|
+
* the wire's own types, and it registers no collection resolver.
|
|
489
|
+
*/
|
|
490
|
+
function createViewModelConverter(options) {
|
|
491
|
+
if (!options?.resolveCollection) return () => void 0;
|
|
492
|
+
return function converterFor(slug) {
|
|
493
|
+
return (values) => {
|
|
494
|
+
const collection = options.resolveCollection?.(slug);
|
|
495
|
+
if (!collection) return values;
|
|
496
|
+
return toViewModelValues(values, collection.properties, collection, options.resolveCollection);
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function buildRebaseData(driver, options) {
|
|
501
|
+
const cache = /* @__PURE__ */ new Map();
|
|
502
|
+
const primaryKeysFor = createPrimaryKeyResolver(options);
|
|
503
|
+
const viewModelFor = createViewModelConverter(options);
|
|
504
|
+
function getAccessor(slug) {
|
|
505
|
+
let accessor = cache.get(slug);
|
|
506
|
+
if (!accessor) {
|
|
507
|
+
accessor = createDriverAccessor(driver, slug, () => primaryKeysFor(slug), viewModelFor(slug), () => options?.resolveDataPath?.(slug) ?? slug);
|
|
508
|
+
cache.set(slug, accessor);
|
|
509
|
+
}
|
|
510
|
+
return accessor;
|
|
511
|
+
}
|
|
512
|
+
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
513
|
+
if (prop === "collection") return getAccessor;
|
|
514
|
+
if (typeof prop === "symbol") return void 0;
|
|
515
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
516
|
+
return getAccessor(options?.resolveAccessor?.(prop) ?? toSnakeCase(prop));
|
|
517
|
+
} });
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Unwrap a Entity back into the flat row it was built from. `rowToEntity` keeps
|
|
521
|
+
* the row untouched under `.values` and derives `.id` alongside it, so dropping
|
|
522
|
+
* the wrapper is the whole operation — the address was never part of the row.
|
|
523
|
+
*/
|
|
524
|
+
function entityToRow(entity) {
|
|
525
|
+
return entity.values;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Fluent query builder for the flat SDK data layer. Mirrors {@link QueryBuilder}
|
|
529
|
+
* but resolves to `FindResult<M>` (flat rows) instead of Entity-wrapped
|
|
530
|
+
* `FindResponse<M>`.
|
|
531
|
+
*/
|
|
532
|
+
var SdkQueryBuilder = class {
|
|
533
|
+
client;
|
|
534
|
+
params = { where: {} };
|
|
535
|
+
constructor(client) {
|
|
536
|
+
this.client = client;
|
|
537
|
+
}
|
|
538
|
+
where(columnOrCondition, operator, value) {
|
|
539
|
+
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
540
|
+
const next = columnOrCondition;
|
|
541
|
+
this.params.logical = this.params.logical ? {
|
|
542
|
+
type: "and",
|
|
543
|
+
conditions: [this.params.logical, next]
|
|
544
|
+
} : next;
|
|
545
|
+
return this;
|
|
546
|
+
}
|
|
547
|
+
if (!this.params.where) this.params.where = {};
|
|
548
|
+
const column = columnOrCondition;
|
|
549
|
+
const condition = [operator, value];
|
|
550
|
+
const existing = this.params.where[column];
|
|
551
|
+
if (existing === void 0) this.params.where[column] = condition;
|
|
552
|
+
else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
|
|
553
|
+
else {
|
|
554
|
+
let firstCondition;
|
|
555
|
+
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
|
|
556
|
+
else firstCondition = ["==", existing];
|
|
557
|
+
this.params.where[column] = [firstCondition, condition];
|
|
558
|
+
}
|
|
559
|
+
return this;
|
|
560
|
+
}
|
|
561
|
+
/** Called again, this adds a tie-breaker rather than replacing the sort. */
|
|
562
|
+
orderBy(column, direction = "asc", nulls) {
|
|
563
|
+
const existing = normalizeOrderBy(this.params.orderBy) ?? [];
|
|
564
|
+
const key = sortKeyToString(column);
|
|
565
|
+
this.params.orderBy = [...existing, nulls ? [
|
|
566
|
+
key,
|
|
567
|
+
direction,
|
|
568
|
+
nulls
|
|
569
|
+
] : [key, direction]];
|
|
570
|
+
return this;
|
|
571
|
+
}
|
|
572
|
+
limit(count) {
|
|
573
|
+
this.params.limit = count;
|
|
574
|
+
return this;
|
|
575
|
+
}
|
|
576
|
+
offset(count) {
|
|
577
|
+
this.params.offset = count;
|
|
578
|
+
return this;
|
|
579
|
+
}
|
|
580
|
+
search(searchString, options) {
|
|
581
|
+
this.params.searchString = searchString;
|
|
582
|
+
if (options?.explain !== void 0) this.params.searchExplain = options.explain;
|
|
583
|
+
return this;
|
|
584
|
+
}
|
|
585
|
+
vectorSearch(property, vector, options) {
|
|
586
|
+
this.params.vectorSearch = {
|
|
587
|
+
property,
|
|
588
|
+
vector,
|
|
589
|
+
...options?.distance !== void 0 && { distance: options.distance },
|
|
590
|
+
...options?.threshold !== void 0 && { threshold: options.threshold }
|
|
591
|
+
};
|
|
592
|
+
return this;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Load relations. Merges rather than replaces, so `.include("author")` then
|
|
596
|
+
* `.include({ comments: { limit: 5 } })` asks for both — a builder call that
|
|
597
|
+
* silently discarded an earlier one is the same defect `where` had.
|
|
598
|
+
*/
|
|
599
|
+
include(...relations) {
|
|
600
|
+
this.params.include = mergeIncludeSpecs(this.params.include, relations);
|
|
601
|
+
return this;
|
|
602
|
+
}
|
|
603
|
+
fields(...columns) {
|
|
604
|
+
this.params.fields = [...this.params.fields ?? [], ...columns];
|
|
605
|
+
return this;
|
|
606
|
+
}
|
|
607
|
+
distinct(enabled = true) {
|
|
608
|
+
this.params.distinct = enabled;
|
|
609
|
+
return this;
|
|
610
|
+
}
|
|
611
|
+
after(cursor) {
|
|
612
|
+
this.params.after = cursor;
|
|
613
|
+
return this;
|
|
614
|
+
}
|
|
615
|
+
async find() {
|
|
616
|
+
return this.client.find(this.params);
|
|
617
|
+
}
|
|
618
|
+
/** Aggregate the matching rows. See {@link SDKCollectionClient.aggregate}. */
|
|
619
|
+
async aggregate(params) {
|
|
620
|
+
return this.client.aggregate({
|
|
621
|
+
...params,
|
|
622
|
+
where: this.params.where,
|
|
623
|
+
logical: this.params.logical,
|
|
624
|
+
searchString: this.params.searchString
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Page through everything this query matches, one row at a time.
|
|
629
|
+
*
|
|
630
|
+
* `.limit()` on the builder becomes the page size, so the ceiling on a
|
|
631
|
+
* single `find()` is not a ceiling on what the query can read.
|
|
632
|
+
*/
|
|
633
|
+
iterate(options) {
|
|
634
|
+
return this.client.iterate({
|
|
635
|
+
...this.params,
|
|
636
|
+
...this.params.limit !== void 0 && { pageSize: this.params.limit },
|
|
637
|
+
...options
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
/** Collect everything this query matches into one array. */
|
|
641
|
+
findAll(options) {
|
|
642
|
+
return this.client.findAll({
|
|
643
|
+
...this.params,
|
|
644
|
+
...this.params.limit !== void 0 && { pageSize: this.params.limit },
|
|
645
|
+
...options
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Count the records matching this query.
|
|
650
|
+
*
|
|
651
|
+
* This used to answer `0` when the client had no `count` — a number, from a
|
|
652
|
+
* source that had not counted anything, indistinguishable from an empty
|
|
653
|
+
* collection. It now does what the client does, which on a source that
|
|
654
|
+
* cannot count is throw and say so.
|
|
655
|
+
*/
|
|
656
|
+
async count() {
|
|
657
|
+
return this.client.count(this.params);
|
|
658
|
+
}
|
|
659
|
+
listen(onUpdate, onError) {
|
|
660
|
+
return this.client.listen(this.params, onUpdate, onError);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
/**
|
|
664
|
+
* Refuse a write option this door cannot honour, by name, before anything is
|
|
665
|
+
* written.
|
|
666
|
+
*
|
|
667
|
+
* The in-process accessor implements the same `SDKCollectionClient` as the HTTP
|
|
668
|
+
* client, whose `ifMatch` is "Honoured on `update` and `delete`" — and its
|
|
669
|
+
* writes took no options at all, so `rebase.data.posts.update(id, data, {
|
|
670
|
+
* ifMatch: staleEtag })` overwrote the row and resolved. A precondition the
|
|
671
|
+
* caller wrote, dropped without a word, is the worst thing a door can do with
|
|
672
|
+
* it. The driver's `save` has no precondition and no idempotency store to hand
|
|
673
|
+
* these to, so they are refused rather than ignored. An `undefined` value is no
|
|
674
|
+
* option, as it is over HTTP: `etagOf(row)` is `undefined` for a row that
|
|
675
|
+
* carries no version, and that means "write unconditionally".
|
|
676
|
+
*/
|
|
677
|
+
function refuseUnhonouredWriteOptions(method, slug, options) {
|
|
678
|
+
const unsupported = ["ifMatch", "idempotencyKey"].filter((key) => options?.[key]);
|
|
679
|
+
if (unsupported.length === 0) return;
|
|
680
|
+
throw new RebaseClientError(`${method}() on "${slug}" in-process (context.data, rebase.data) cannot honour ${unsupported.join(" or ")}: this door has no ${unsupported.includes("ifMatch") ? "precondition check" : "idempotency store"}, and ignoring the option would write as though it had been checked. Drop it here, or send the write through the HTTP client, which honours it.`, {
|
|
681
|
+
code: "UNSUPPORTED_OPTION",
|
|
682
|
+
details: {
|
|
683
|
+
method,
|
|
684
|
+
options: unsupported
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Wrap a Entity-shaped {@link CollectionAccessor} into a flat
|
|
690
|
+
* {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
|
|
691
|
+
* so the backend SDK is byte-for-byte the same shape as the frontend client.
|
|
692
|
+
*/
|
|
693
|
+
function toSdkCollectionClient(snap, slug = "collection") {
|
|
694
|
+
const client = {
|
|
695
|
+
async find(params) {
|
|
696
|
+
const res = await snap.find(params);
|
|
697
|
+
return {
|
|
698
|
+
data: res.data.map(entityToRow),
|
|
699
|
+
meta: res.meta
|
|
700
|
+
};
|
|
701
|
+
},
|
|
702
|
+
iterate(params) {
|
|
703
|
+
return paginateFind((p) => client.find(p), params, slug);
|
|
704
|
+
},
|
|
705
|
+
findAll(params) {
|
|
706
|
+
return collectAllPages((p) => client.find(p), params, slug);
|
|
707
|
+
},
|
|
708
|
+
async findById(id, options) {
|
|
709
|
+
const s = await snap.findById(id, options);
|
|
710
|
+
return s ? entityToRow(s) : void 0;
|
|
711
|
+
},
|
|
712
|
+
async get(id) {
|
|
713
|
+
const s = await snap.findById(id);
|
|
714
|
+
if (!s) throw new RebaseApiError(`No record with id ${JSON.stringify(String(id))} in "${slug}".`, {
|
|
715
|
+
status: 404,
|
|
716
|
+
code: "NOT_FOUND"
|
|
717
|
+
});
|
|
718
|
+
return entityToRow(s);
|
|
719
|
+
},
|
|
720
|
+
async create(data, id, options) {
|
|
721
|
+
refuseUnhonouredWriteOptions("create", slug, options);
|
|
722
|
+
return entityToRow(await snap.create(data, id));
|
|
723
|
+
},
|
|
724
|
+
async createMany(data, options) {
|
|
725
|
+
refuseUnhonouredWriteOptions("createMany", slug, options);
|
|
726
|
+
if (!Array.isArray(data)) throw new RebaseClientError("createMany expects an array of records.", { code: "INVALID_BULK_BODY" });
|
|
727
|
+
if (data.length === 0) return [];
|
|
728
|
+
if (!snap.createMany) throw new Error("Bulk writes are not supported by this collection's data source. Fall back to create() per record.");
|
|
729
|
+
return (await snap.createMany(data, {
|
|
730
|
+
upsert: options?.upsert,
|
|
731
|
+
onConflict: options?.onConflict
|
|
732
|
+
})).map(entityToRow);
|
|
733
|
+
},
|
|
734
|
+
/**
|
|
735
|
+
* One row through the bulk path, because the bulk path is where the
|
|
736
|
+
* conflict target lives.
|
|
737
|
+
*
|
|
738
|
+
* `CollectionAccessor` has no single-row upsert and adding one would
|
|
739
|
+
* mean a second way to say the same thing to the same driver method —
|
|
740
|
+
* `saveMany` already takes `upsert` and `onConflict`, and a batch of
|
|
741
|
+
* one is exactly an upsert of one.
|
|
742
|
+
*/
|
|
743
|
+
async upsert(data, options) {
|
|
744
|
+
refuseUnhonouredWriteOptions("upsert", slug, options);
|
|
745
|
+
if (!snap.createMany) throw new Error("Upsert is not supported by this collection's data source: it needs a bulk write, which this driver does not implement. Fall back to create() or update().");
|
|
746
|
+
const row = (await snap.createMany([data], {
|
|
747
|
+
upsert: true,
|
|
748
|
+
onConflict: options?.onConflict
|
|
749
|
+
}))[0];
|
|
750
|
+
if (!row) throw new Error(`Upsert into "${slug}" returned no row.`);
|
|
751
|
+
return entityToRow(row);
|
|
752
|
+
},
|
|
753
|
+
async update(id, data, options) {
|
|
754
|
+
refuseUnhonouredWriteOptions("update", slug, options);
|
|
755
|
+
return entityToRow(await snap.update(id, data));
|
|
756
|
+
},
|
|
757
|
+
async updateMany(updates, options) {
|
|
758
|
+
refuseUnhonouredWriteOptions("updateMany", slug, options);
|
|
759
|
+
if (!Array.isArray(updates)) throw new RebaseClientError("updateMany expects an array of { id, data } entries.", { code: "INVALID_BULK_BODY" });
|
|
760
|
+
if (updates.length === 0) return [];
|
|
761
|
+
if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
|
|
762
|
+
return (await snap.updateMany(updates.map((u) => ({
|
|
763
|
+
id: u.id,
|
|
764
|
+
data: u.data
|
|
765
|
+
})))).map(entityToRow);
|
|
766
|
+
},
|
|
767
|
+
async delete(id, options) {
|
|
768
|
+
refuseUnhonouredWriteOptions("delete", slug, options);
|
|
769
|
+
return snap.delete(id);
|
|
770
|
+
},
|
|
771
|
+
async deleteMany(ids, options) {
|
|
772
|
+
refuseUnhonouredWriteOptions("deleteMany", slug, options);
|
|
773
|
+
if (!Array.isArray(ids)) throw new RebaseClientError("deleteMany expects an array of ids.", { code: "INVALID_BULK_BODY" });
|
|
774
|
+
if (ids.length === 0) return;
|
|
775
|
+
if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
|
|
776
|
+
await snap.deleteMany(ids);
|
|
777
|
+
},
|
|
778
|
+
count: snap.count ? (params) => snap.count(params) : unsupportedMethod(noCount(slug)),
|
|
779
|
+
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
780
|
+
data: res.data.map(entityToRow),
|
|
781
|
+
meta: res.meta
|
|
782
|
+
}), onError) : unsupportedMethod(noRealtime(slug)),
|
|
783
|
+
listenById: snap.listenById ? (id, onUpdate, onError, options) => snap.listenById(id, (s) => onUpdate(s ? entityToRow(s) : void 0), onError, options) : unsupportedMethod(noRealtime(slug)),
|
|
784
|
+
listenCount: snap.listenCount ? (params, onCount, onError) => snap.listenCount(params, onCount, onError) : unsupportedMethod(noRealtime(slug)),
|
|
785
|
+
where(columnOrCondition, operator, value) {
|
|
786
|
+
const builder = new SdkQueryBuilder(client);
|
|
787
|
+
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
788
|
+
return builder.where(columnOrCondition, operator, value);
|
|
789
|
+
},
|
|
790
|
+
orderBy: (column, direction, nulls) => new SdkQueryBuilder(client).orderBy(column, direction, nulls),
|
|
791
|
+
limit: (count) => new SdkQueryBuilder(client).limit(count),
|
|
792
|
+
offset: (count) => new SdkQueryBuilder(client).offset(count),
|
|
793
|
+
search: (searchString) => new SdkQueryBuilder(client).search(searchString),
|
|
794
|
+
vectorSearch: (property, vector, options) => new SdkQueryBuilder(client).vectorSearch(property, vector, options),
|
|
795
|
+
include: (...relations) => new SdkQueryBuilder(client).include(...relations),
|
|
796
|
+
fields: (...columns) => new SdkQueryBuilder(client).fields(...columns),
|
|
797
|
+
distinct: (enabled) => new SdkQueryBuilder(client).distinct(enabled),
|
|
798
|
+
after: (cursor) => new SdkQueryBuilder(client).after(cursor),
|
|
799
|
+
aggregate: snap.aggregate ? (params) => snap.aggregate(params) : unsupportedMethod(noAggregate(slug)),
|
|
800
|
+
trash: toSdkTrashClient(snap, slug, () => client)
|
|
801
|
+
};
|
|
802
|
+
return client;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* {@link SDKCollectionClient.trash} over an accessor's
|
|
806
|
+
* {@link CollectionAccessor.trash}: the same API the browser client has. A
|
|
807
|
+
* restore is the client's own `update`/`updateMany` setting the soft-delete
|
|
808
|
+
* field to `null`, so it keeps their refusals and their answer.
|
|
809
|
+
*/
|
|
810
|
+
function toSdkTrashClient(snap, slug, client) {
|
|
811
|
+
const trash = snap.trash;
|
|
812
|
+
if (!trash) return {
|
|
813
|
+
find: unsupportedMethod(noTrash(slug)),
|
|
814
|
+
count: unsupportedMethod(noTrash(slug)),
|
|
815
|
+
restore: unsupportedMethod(noTrash(slug)),
|
|
816
|
+
restoreMany: unsupportedMethod(noTrash(slug)),
|
|
817
|
+
purge: unsupportedMethod(noTrash(slug)),
|
|
818
|
+
purgeMany: unsupportedMethod(noTrash(slug))
|
|
819
|
+
};
|
|
820
|
+
const field = (options) => options?.field ?? DEFAULT_SOFT_DELETE_FIELD;
|
|
821
|
+
return {
|
|
822
|
+
async find(params) {
|
|
823
|
+
const res = await trash.find(params);
|
|
824
|
+
return {
|
|
825
|
+
data: res.data.map(entityToRow),
|
|
826
|
+
meta: res.meta
|
|
827
|
+
};
|
|
828
|
+
},
|
|
829
|
+
count: (params) => trash.count(params),
|
|
830
|
+
restore: (id, options) => client().update(id, { [field(options)]: null }, writeOptionsOf(options)),
|
|
831
|
+
restoreMany(ids, options) {
|
|
832
|
+
if (!Array.isArray(ids)) return Promise.reject(new RebaseClientError("trash.restoreMany expects an array of ids.", { code: "INVALID_BULK_BODY" }));
|
|
833
|
+
const data = { [field(options)]: null };
|
|
834
|
+
return client().updateMany(ids.map((id) => ({
|
|
835
|
+
id,
|
|
836
|
+
data
|
|
837
|
+
})), writeOptionsOf(options));
|
|
838
|
+
},
|
|
839
|
+
async purge(id, options) {
|
|
840
|
+
refuseUnhonouredWriteOptions("trash.purge", slug, options);
|
|
841
|
+
await trash.purge(id);
|
|
842
|
+
},
|
|
843
|
+
async purgeMany(ids, options) {
|
|
844
|
+
refuseUnhonouredWriteOptions("trash.purgeMany", slug, options);
|
|
845
|
+
if (!Array.isArray(ids)) throw new RebaseClientError("trash.purgeMany expects an array of ids.", { code: "INVALID_BULK_BODY" });
|
|
846
|
+
if (ids.length === 0) return;
|
|
847
|
+
if (!trash.purgeMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to trash.purge() per record.");
|
|
848
|
+
await trash.purgeMany(ids);
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
/** A {@link RestoreOptions} without its `field`: the write options it carries. */
|
|
853
|
+
function writeOptionsOf(options) {
|
|
854
|
+
if (!options) return void 0;
|
|
855
|
+
const { field: _field, ...rest } = options;
|
|
856
|
+
return rest;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
|
|
860
|
+
* {@link CollectionAccessor}. Every returned row is re-wrapped into the
|
|
861
|
+
* `{ id, path, values }` view-model the admin panel renders.
|
|
862
|
+
*/
|
|
863
|
+
function toEntityAccessor(sdk, slug, getPks = () => [], toViewModel, viewModelSource = () => void 0) {
|
|
864
|
+
const accessor = {
|
|
865
|
+
async find(params) {
|
|
866
|
+
const res = await sdk.find(params);
|
|
867
|
+
return {
|
|
868
|
+
data: res.data.map((row) => rowToEntity(row, slug, getPks(), toViewModel)),
|
|
869
|
+
meta: res.meta
|
|
870
|
+
};
|
|
871
|
+
},
|
|
872
|
+
async findById(id, options) {
|
|
873
|
+
const row = await sdk.findById(id, options);
|
|
874
|
+
return row ? rowToEntity(row, slug, getPks(), toViewModel) : void 0;
|
|
875
|
+
},
|
|
876
|
+
async create(data, id) {
|
|
877
|
+
return rowToEntity(await sdk.create(data, id), slug, getPks(), toViewModel);
|
|
878
|
+
},
|
|
879
|
+
createMany: sdk.createMany ? async (data, options) => {
|
|
880
|
+
return (await sdk.createMany(data, options)).map((row) => rowToEntity(row, slug, getPks(), toViewModel));
|
|
881
|
+
} : void 0,
|
|
882
|
+
async update(id, data, options) {
|
|
883
|
+
const row = options?.ifMatch ? await sdk.update(id, data, { ifMatch: options.ifMatch }) : await sdk.update(id, data);
|
|
884
|
+
if (!row) throw new Error(`Update returned no data for id ${id}`);
|
|
885
|
+
return rowToEntity(row, slug, getPks(), toViewModel);
|
|
886
|
+
},
|
|
887
|
+
delete(id) {
|
|
888
|
+
return sdk.delete(id);
|
|
889
|
+
},
|
|
890
|
+
count: isUnsupported(sdk.count) ? void 0 : (params) => sdk.count(params),
|
|
891
|
+
aggregate: isUnsupported(sdk.aggregate) ? void 0 : (params) => sdk.aggregate(params),
|
|
892
|
+
listen: isUnsupported(sdk.listen) ? void 0 : (params, onUpdate, onError) => {
|
|
893
|
+
let built = /* @__PURE__ */ new WeakMap();
|
|
894
|
+
let builtWith;
|
|
895
|
+
return sdk.listen(params, (res) => {
|
|
896
|
+
const pks = getPks();
|
|
897
|
+
const source = viewModelSource();
|
|
898
|
+
if (!builtWith || builtWith[0] !== pks || builtWith[1] !== source) {
|
|
899
|
+
built = /* @__PURE__ */ new WeakMap();
|
|
900
|
+
builtWith = [pks, source];
|
|
901
|
+
}
|
|
902
|
+
onUpdate({
|
|
903
|
+
data: res.data.map((row) => {
|
|
904
|
+
let entity = built.get(row);
|
|
905
|
+
if (!entity) {
|
|
906
|
+
entity = rowToEntity(row, slug, pks, toViewModel);
|
|
907
|
+
built.set(row, entity);
|
|
908
|
+
}
|
|
909
|
+
return entity;
|
|
910
|
+
}),
|
|
911
|
+
meta: res.meta
|
|
912
|
+
});
|
|
913
|
+
}, onError);
|
|
914
|
+
},
|
|
915
|
+
listenById: isUnsupported(sdk.listenById) ? void 0 : (id, onUpdate, onError, options) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug, getPks(), toViewModel) : void 0), onError, options),
|
|
916
|
+
listenCount: isUnsupported(sdk.listenCount) ? void 0 : (params, onCount, onError) => sdk.listenCount(params, onCount, onError),
|
|
917
|
+
trash: !sdk.trash || isUnsupported(sdk.trash.find) ? void 0 : {
|
|
918
|
+
async find(params) {
|
|
919
|
+
const res = await sdk.trash.find(params);
|
|
920
|
+
return {
|
|
921
|
+
data: res.data.map((row) => rowToEntity(row, slug, getPks(), toViewModel)),
|
|
922
|
+
meta: res.meta
|
|
923
|
+
};
|
|
924
|
+
},
|
|
925
|
+
count: (params) => sdk.trash.count(params),
|
|
926
|
+
purge: (id) => sdk.trash.purge(id),
|
|
927
|
+
purgeMany: (ids) => sdk.trash.purgeMany(ids)
|
|
928
|
+
},
|
|
929
|
+
where(columnOrCondition, operator, value) {
|
|
930
|
+
const builder = new QueryBuilder(accessor);
|
|
931
|
+
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
932
|
+
return builder.where(columnOrCondition, operator, value);
|
|
933
|
+
},
|
|
934
|
+
orderBy: (column, direction) => new QueryBuilder(accessor).orderBy(column, direction),
|
|
935
|
+
limit: (count) => new QueryBuilder(accessor).limit(count),
|
|
936
|
+
offset: (count) => new QueryBuilder(accessor).offset(count),
|
|
937
|
+
search: (searchString) => new QueryBuilder(accessor).search(searchString),
|
|
938
|
+
vectorSearch: (property, vector, options) => new QueryBuilder(accessor).vectorSearch(property, vector, options),
|
|
939
|
+
include: (...relations) => new QueryBuilder(accessor).include(...relations)
|
|
940
|
+
};
|
|
941
|
+
return accessor;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Wrap a flat {@link RebaseSdkData} into a Entity-shaped {@link RebaseData}.
|
|
945
|
+
*
|
|
946
|
+
* This is the **admin boundary**: the SDK client (`client.data`) returns flat
|
|
947
|
+
* rows, but the admin renders the `Entity` view-model (`entity.values.*`).
|
|
948
|
+
* `core/Rebase.tsx` wraps `client.data` through this before handing it to the
|
|
949
|
+
* admin `RebaseDataContext` — without it the admin renders rows with only their
|
|
950
|
+
* `id`.
|
|
951
|
+
*/
|
|
952
|
+
/**
|
|
953
|
+
* Only the by-slug accessor is asked for, so only that is required.
|
|
954
|
+
*
|
|
955
|
+
* Taking a whole `RebaseSdkData` meant taking `RebaseSdkData<unknown>`, whose
|
|
956
|
+
* dynamic branch is an index signature — and no `RebaseSdkData<DB>` satisfies
|
|
957
|
+
* it, because its own `collection` method is not a `SDKCollectionClient`. So a
|
|
958
|
+
* caller holding a *typed* client could not pass it to a function that reads
|
|
959
|
+
* one method off it, and that method is identical on every instantiation.
|
|
960
|
+
*/
|
|
961
|
+
function wrapAsEntityData(sdkData, options) {
|
|
962
|
+
const cache = /* @__PURE__ */ new Map();
|
|
963
|
+
const primaryKeysFor = createPrimaryKeyResolver(options);
|
|
964
|
+
const viewModelFor = createViewModelConverter(options);
|
|
965
|
+
function getAccessor(slug) {
|
|
966
|
+
let accessor = cache.get(slug);
|
|
967
|
+
if (!accessor) {
|
|
968
|
+
accessor = toEntityAccessor(sdkData.collection(slug), slug, () => primaryKeysFor(slug), viewModelFor(slug), () => options?.resolveCollection?.(slug));
|
|
969
|
+
cache.set(slug, accessor);
|
|
970
|
+
}
|
|
971
|
+
return accessor;
|
|
972
|
+
}
|
|
973
|
+
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
974
|
+
if (prop === "collection") return getAccessor;
|
|
975
|
+
if (typeof prop === "symbol") return void 0;
|
|
976
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
977
|
+
return getAccessor(toSnakeCase(prop));
|
|
978
|
+
} });
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Wrap a Entity-shaped {@link RebaseData} into a flat {@link RebaseSdkData}.
|
|
982
|
+
*
|
|
983
|
+
* Every collection accessor is adapted to return flat rows. Use this to derive
|
|
984
|
+
* the flat SDK data layer (`context.data`) from an existing Entity data layer
|
|
985
|
+
* — e.g. the admin routes its Entity data via `useData()` and exposes the
|
|
986
|
+
* same routing as flat `context.data` for callbacks by wrapping it here.
|
|
987
|
+
*/
|
|
988
|
+
function wrapAsSdkData(entityData, options) {
|
|
989
|
+
const cache = /* @__PURE__ */ new Map();
|
|
990
|
+
function getAccessor(slug) {
|
|
991
|
+
let accessor = cache.get(slug);
|
|
992
|
+
if (!accessor) {
|
|
993
|
+
accessor = toSdkCollectionClient(entityData.collection(slug), slug);
|
|
994
|
+
cache.set(slug, accessor);
|
|
995
|
+
}
|
|
996
|
+
return accessor;
|
|
997
|
+
}
|
|
998
|
+
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
999
|
+
if (prop === "collection") return getAccessor;
|
|
1000
|
+
if (typeof prop === "symbol") return void 0;
|
|
1001
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
1002
|
+
return getAccessor(options?.resolveAccessor?.(prop) ?? toSnakeCase(prop));
|
|
1003
|
+
} });
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Build a flat {@link RebaseSdkData} from a `DataDriver`.
|
|
1007
|
+
*
|
|
1008
|
+
* This is the developer-facing SDK data layer used by backend framework
|
|
1009
|
+
* callbacks & scripts (`context.data` / `rebase.dataAsAdmin`). It returns flat rows —
|
|
1010
|
+
* identical in shape to the frontend SDK client, down to how a relation is
|
|
1011
|
+
* served: a foreign key stays a foreign key, and a relation named in `include`
|
|
1012
|
+
* arrives as the target's own columns. The `{ __type: "relation" }` envelope is
|
|
1013
|
+
* the admin's view-model and never reaches here.
|
|
1014
|
+
*
|
|
1015
|
+
* The admin uses {@link buildRebaseData} (Entity) over its own driver.
|
|
1016
|
+
*
|
|
1017
|
+
* `options.resolveAccessor` names the slug a property is (see
|
|
1018
|
+
* `createAccessorSlugResolver`); absent, `data.myNotes` is `my_notes`.
|
|
1019
|
+
*/
|
|
1020
|
+
function buildSdkData(driver, options) {
|
|
1021
|
+
return wrapAsSdkData(buildRebaseData(driver), options);
|
|
1022
|
+
}
|
|
1023
|
+
//#endregion
|
|
1024
|
+
export { aggregateAlias, buildRebaseData, buildSdkData, wrapAsEntityData, wrapAsSdkData };
|
|
1025
|
+
|
|
1026
|
+
//# sourceMappingURL=buildRebaseData.es.js.map
|