@rebasepro/server-postgres 0.19.1 → 0.19.2-canary.g08eed46
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/{BranchService-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
- package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +81 -7
- package/dist/PostgresBootstrapper.d.ts +10 -0
- package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
- package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
- package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
- package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
- package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
- package/dist/cli-errors-C3g_kHBw.js.map +1 -0
- package/dist/cli-errors.d.ts +19 -0
- package/dist/cli-helpers.d.ts +47 -0
- package/dist/cli.js +233 -19
- package/dist/cli.js.map +1 -1
- package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
- package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
- package/dist/collections/validate-relations.d.ts +7 -0
- package/dist/column-plan-helpers-CpILzHJS.js +145 -0
- package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
- package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
- package/dist/doctor-D5SrGJ5P.js.map +1 -0
- package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
- package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
- package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
- package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
- package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
- package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
- package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
- package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
- package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
- package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +23 -4
- package/dist/index.es.js +1991 -580
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
- package/dist/plan-schema-Hgl62S-w.js +1531 -0
- package/dist/plan-schema-Hgl62S-w.js.map +1 -0
- package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
- package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
- package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
- package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
- package/dist/schema/catalogue-schema.d.ts +97 -0
- package/dist/schema/collection-index.d.ts +9 -0
- package/dist/schema/column-plan-helpers.d.ts +125 -0
- package/dist/schema/config-relations.d.ts +32 -0
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +9 -0
- package/dist/schema/dynamic-tables.d.ts +27 -6
- package/dist/schema/ensure-collection-tables.d.ts +31 -268
- package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
- package/dist/schema/generate-postgres-ddl.js +8 -2
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/generated-column-conflicts.d.ts +84 -0
- package/dist/schema/generated-schema-diff.d.ts +56 -0
- package/dist/schema/generated-schema-staleness.d.ts +33 -0
- package/dist/schema/introspect-db-logic.d.ts +2 -0
- package/dist/schema/plan/diff-plan.d.ts +220 -0
- package/dist/schema/plan/plan-schema.d.ts +84 -0
- package/dist/schema/plan/render-ddl.d.ts +62 -0
- package/dist/schema/plan/render-drizzle.d.ts +31 -0
- package/dist/schema/plan/types.d.ts +410 -0
- package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
- package/dist/schema/relation-names.d.ts +28 -0
- package/dist/schema/search-column.d.ts +9 -0
- package/dist/security/rls-enforcement.d.ts +17 -0
- package/dist/services/FetchService.d.ts +176 -36
- package/dist/services/PersistService.d.ts +17 -4
- package/dist/services/RelationService.d.ts +11 -3
- package/dist/services/collection-helpers.d.ts +16 -2
- package/dist/services/dataService.d.ts +29 -2
- package/dist/services/field-op-sql.d.ts +16 -0
- package/dist/services/field-viewer.d.ts +19 -0
- package/dist/services/realtimeService.d.ts +35 -1
- package/dist/services/row-pipeline.d.ts +29 -5
- package/dist/services/soft-delete.d.ts +73 -0
- package/dist/utils/pg-error-utils.d.ts +41 -1
- package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
- package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
- package/package.json +18 -10
- package/dist/cli-errors-DxN4yzdH.js.map +0 -1
- package/dist/doctor-D7HWmGzs.js.map +0 -1
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
- package/dist/rls-enforcement-DeYEjYLt.js.map +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { SQL } from "drizzle-orm";
|
|
2
2
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
3
3
|
import { FilterValues, OrderByTuple, LogicalCondition } from "@rebasepro/types";
|
|
4
|
-
import type { VectorSearchParams } from "@rebasepro/types";
|
|
4
|
+
import type { IncludeSpec, VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { RelationService } from "./RelationService.js";
|
|
6
6
|
import { DrizzleClient } from "../interfaces.js";
|
|
7
7
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
|
|
8
8
|
import { type NestedPathHop } from "./nested-path.js";
|
|
9
|
+
import { type WithDeleted } from "./soft-delete.js";
|
|
9
10
|
/**
|
|
10
11
|
* Service for handling all row read operations.
|
|
11
12
|
* Handles fetching, searching, counting, and filtering rows.
|
|
@@ -99,6 +100,21 @@ export declare class FetchService {
|
|
|
99
100
|
* one key of several still honours the ones that did resolve.
|
|
100
101
|
*/
|
|
101
102
|
private resolveOrderKeys;
|
|
103
|
+
/**
|
|
104
|
+
* Whether this key's NULLs sort *after* its real values.
|
|
105
|
+
*
|
|
106
|
+
* The default is Postgres's own — `NULLS LAST` ascending, `NULLS FIRST`
|
|
107
|
+
* descending — and it was previously hardcoded in two places that had to
|
|
108
|
+
* agree: the `ORDER BY` and the keyset comparison behind cursor paging. A
|
|
109
|
+
* key that states a placement overrides it, in both, because they read the
|
|
110
|
+
* answer from here.
|
|
111
|
+
*
|
|
112
|
+
* The default is also the thing a "newest first" list gets wrong: every row
|
|
113
|
+
* with no date sorts to the very top, ahead of everything real, and the only
|
|
114
|
+
* way out used to be an `is-not-null` filter that dropped those rows
|
|
115
|
+
* entirely. `orderBy: [["published_at", "desc", "last"]]` is the fix.
|
|
116
|
+
*/
|
|
117
|
+
private static nullsLast;
|
|
102
118
|
/**
|
|
103
119
|
* The full `ORDER BY`: the caller's keys, then the id.
|
|
104
120
|
*
|
|
@@ -138,21 +154,95 @@ export declare class FetchService {
|
|
|
138
154
|
*/
|
|
139
155
|
private getJunctionTargetRelationName;
|
|
140
156
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
157
|
+
* The relations one level of an include tree names, resolved.
|
|
158
|
+
*
|
|
159
|
+
* A name that is not a relation is a **400 `UNKNOWN_RELATION`**. It used to
|
|
160
|
+
* be dropped silently, which answers 200 with the field missing — and a
|
|
161
|
+
* missing relation field is indistinguishable from a row that genuinely has
|
|
162
|
+
* no related row, so a typo in an `include` looked like empty data.
|
|
144
163
|
*/
|
|
145
|
-
private
|
|
164
|
+
private resolveIncludeLevel;
|
|
165
|
+
/**
|
|
166
|
+
* The extra `WHERE` an include's own `where`/`logical` puts on the target.
|
|
167
|
+
*
|
|
168
|
+
* Pushed into the batch query rather than applied to the rows it returns:
|
|
169
|
+
* filtering afterwards reads every related row of every parent to throw
|
|
170
|
+
* most of them away, and on a to-many relation that is the whole table.
|
|
171
|
+
*/
|
|
172
|
+
private includeNarrowing;
|
|
146
173
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
174
|
+
* Sort, cap and project the rows of ONE to-many relation, per parent.
|
|
175
|
+
*
|
|
176
|
+
* `orderBy` and `limit` are applied here rather than in SQL because a limit
|
|
177
|
+
* on a batched relation load is *per parent*, and expressing that needs a
|
|
178
|
+
* LATERAL or a window function per relation kind — four of them, each with
|
|
179
|
+
* its own join shape. The rows were already narrowed by
|
|
180
|
+
* {@link includeNarrowing} in SQL, so what this sorts and cuts is the set
|
|
181
|
+
* the caller asked for and not the table.
|
|
182
|
+
*/
|
|
183
|
+
private shapeRelatedRows;
|
|
184
|
+
/**
|
|
185
|
+
* Load an include tree onto `rows`, in place, one batched query per
|
|
186
|
+
* relation per level.
|
|
187
|
+
*
|
|
188
|
+
* Recurses into nested includes with the rows it just loaded, so
|
|
189
|
+
* `comments.author` is two queries for a page of posts rather than one per
|
|
190
|
+
* comment. The depth bound lives on the normalizer
|
|
191
|
+
* ({@link MAX_INCLUDE_DEPTH}); this asserts it again because the tree can
|
|
192
|
+
* also be built in-process, where nothing normalized it.
|
|
193
|
+
*/
|
|
194
|
+
private loadIncludes;
|
|
195
|
+
/**
|
|
196
|
+
* The SELECT list for a read: the visible columns, narrowed to `fields`.
|
|
197
|
+
*
|
|
198
|
+
* `?fields=` used to be a trim of the *response* — every column read out of
|
|
199
|
+
* the database and most of them thrown away in JavaScript. It is a
|
|
200
|
+
* projection now, so asking for two columns of a wide row reads two
|
|
201
|
+
* columns; that is also what makes `distinct` mean anything.
|
|
202
|
+
*
|
|
203
|
+
* Two things survive a narrowing regardless:
|
|
204
|
+
*
|
|
205
|
+
* - the **primary key**, because a row nobody can address cannot be
|
|
206
|
+
* updated, deleted, or paged past — and `meta.nextCursor` is derived from
|
|
207
|
+
* it, so a projection without it would silently disable seeking. The one
|
|
208
|
+
* exception is `distinct`, below;
|
|
209
|
+
* - the exclusions. `excludeFromApi` and the generated search columns are
|
|
210
|
+
* removed *after* the narrowing, so naming one in `fields` does not
|
|
211
|
+
* un-hide it. That was the shape of the `?searchString=` leak: a path
|
|
212
|
+
* that skipped the projection returned a password hash the plain read
|
|
213
|
+
* correctly withheld.
|
|
214
|
+
*
|
|
215
|
+
* An unknown column name is a 400 rather than a silent omission: a caller
|
|
216
|
+
* who mistypes `?fields=titel` otherwise gets rows without titles and no
|
|
217
|
+
* hint why.
|
|
218
|
+
*
|
|
219
|
+
* **`distinct` drops the primary key.** Keeping it is what the refusal of
|
|
220
|
+
* `distinct` beside a search or vector query already describes: a value
|
|
221
|
+
* that differs on every row makes the whole row distinct by construction,
|
|
222
|
+
* so the query "would answer 200 having done nothing". A surrogate key does
|
|
223
|
+
* that more reliably than any score — `?fields=status&distinct=true` came
|
|
224
|
+
* back with one row per row, every one carrying its `id`. So a distinct
|
|
225
|
+
* read is a read of the named columns and nothing else. It addresses no
|
|
226
|
+
* rows, which is the honest shape for one: `cursorFor` finds no key on the
|
|
227
|
+
* row and issues no cursor, and the result is a set of values rather than a
|
|
228
|
+
* set of rows to update or delete.
|
|
229
|
+
*/
|
|
230
|
+
private columnProjection;
|
|
231
|
+
/**
|
|
232
|
+
* The opaque cursor that continues a listing after `row`. See
|
|
233
|
+
* {@link RestFetchService.cursorFor}.
|
|
234
|
+
*
|
|
235
|
+
* Here rather than at the route because deriving it needs the collection's
|
|
236
|
+
* primary key, which may be named anything and span several columns — the
|
|
237
|
+
* driver's knowledge, not the HTTP layer's.
|
|
149
238
|
*/
|
|
150
|
-
|
|
239
|
+
cursorFor(collectionPath: string, row: Record<string, unknown>, orderBy?: OrderByTuple[]): string | undefined;
|
|
151
240
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
241
|
+
* Post-fetch joinPath relations for a single flat row.
|
|
242
|
+
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
243
|
+
* so they must be loaded separately after the primary query.
|
|
154
244
|
*/
|
|
155
|
-
private
|
|
245
|
+
private resolveJoinPathRelations;
|
|
156
246
|
/**
|
|
157
247
|
* Extract cursor pagination conditions from startAfter options.
|
|
158
248
|
*
|
|
@@ -195,7 +285,7 @@ export declare class FetchService {
|
|
|
195
285
|
/**
|
|
196
286
|
* Fetch a single row by ID
|
|
197
287
|
*/
|
|
198
|
-
fetchOne<M extends Record<string, unknown>>(collectionPath: string, id: string | number, databaseId?: string): Promise<Record<string, unknown> | undefined>;
|
|
288
|
+
fetchOne<M extends Record<string, unknown>>(collectionPath: string, id: string | number, databaseId?: string, withDeleted?: WithDeleted): Promise<Record<string, unknown> | undefined>;
|
|
199
289
|
/**
|
|
200
290
|
* Unified method to fetch rows with optional search functionality
|
|
201
291
|
*/
|
|
@@ -213,6 +303,23 @@ export declare class FetchService {
|
|
|
213
303
|
logical?: LogicalCondition;
|
|
214
304
|
/** Narrow to the rows reachable from a parent through a relation. */
|
|
215
305
|
relatedTo?: NestedPathHop;
|
|
306
|
+
/** See `FetchCollectionProps.withDeleted`. */
|
|
307
|
+
withDeleted?: WithDeleted;
|
|
308
|
+
/**
|
|
309
|
+
* Relations to load. **Absent means none** — the same as it means
|
|
310
|
+
* over REST.
|
|
311
|
+
*
|
|
312
|
+
* It used to mean *all of them*, unconditionally, and this is the
|
|
313
|
+
* method the realtime refetch goes through. So a subscription was
|
|
314
|
+
* pushed rows carrying every relation the collection declares while
|
|
315
|
+
* the identical `find()` returned rows carrying none, and a client
|
|
316
|
+
* that rendered both saw the row change shape when a write landed.
|
|
317
|
+
*/
|
|
318
|
+
include?: IncludeSpec;
|
|
319
|
+
/** Columns to read — a projection pushed into the SELECT. */
|
|
320
|
+
fields?: string[];
|
|
321
|
+
/** `SELECT DISTINCT` over the projection. */
|
|
322
|
+
distinct?: boolean;
|
|
216
323
|
}): Promise<Record<string, unknown>[]>;
|
|
217
324
|
/**
|
|
218
325
|
* Fallback path used when db.query is unavailable.
|
|
@@ -245,8 +352,15 @@ export declare class FetchService {
|
|
|
245
352
|
offset?: number;
|
|
246
353
|
startAfter?: Record<string, unknown>;
|
|
247
354
|
searchString?: string;
|
|
355
|
+
searchExplain?: boolean;
|
|
248
356
|
databaseId?: string;
|
|
249
357
|
vectorSearch?: VectorSearchParams;
|
|
358
|
+
/** Relations to load. Absent means none — see `fetchRowsWithConditions`. */
|
|
359
|
+
include?: IncludeSpec;
|
|
360
|
+
/** Columns to read — a projection pushed into the SELECT. */
|
|
361
|
+
fields?: string[];
|
|
362
|
+
/** `SELECT DISTINCT` over the projection. */
|
|
363
|
+
distinct?: boolean;
|
|
250
364
|
}): Promise<Record<string, unknown>[]>;
|
|
251
365
|
/**
|
|
252
366
|
* Search rows by text
|
|
@@ -285,6 +399,8 @@ export declare class FetchService {
|
|
|
285
399
|
* request that was served three rows was told there were nine.
|
|
286
400
|
*/
|
|
287
401
|
vectorSearch?: VectorSearchParams;
|
|
402
|
+
/** See `FetchCollectionProps.withDeleted`. */
|
|
403
|
+
withDeleted?: WithDeleted;
|
|
288
404
|
}): Promise<number>;
|
|
289
405
|
/**
|
|
290
406
|
* `count`/`sum`/`avg`/`min`/`max`, optionally grouped.
|
|
@@ -312,6 +428,8 @@ export declare class FetchService {
|
|
|
312
428
|
logical?: LogicalCondition;
|
|
313
429
|
searchString?: string;
|
|
314
430
|
limit?: number;
|
|
431
|
+
/** See `FetchCollectionProps.withDeleted`. */
|
|
432
|
+
withDeleted?: WithDeleted;
|
|
315
433
|
}): Promise<Record<string, unknown>[]>;
|
|
316
434
|
/**
|
|
317
435
|
* Check if a field value is unique
|
|
@@ -322,12 +440,24 @@ export declare class FetchService {
|
|
|
322
440
|
*/
|
|
323
441
|
getRelationService(): RelationService;
|
|
324
442
|
/**
|
|
325
|
-
* Fetch a collection of rows with
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
443
|
+
* Fetch a collection of rows, with the relations `include` names.
|
|
444
|
+
*
|
|
445
|
+
* ## Why this no longer uses `db.query.findMany({ with })`
|
|
446
|
+
*
|
|
447
|
+
* It used to, whenever a `db.query` builder existed for the table — which
|
|
448
|
+
* is Drizzle's relational API, and which compiles a to-many relation into a
|
|
449
|
+
* lateral join. This file's own comment on the *other* read path says what
|
|
450
|
+
* that costs: "catastrophically slow for large collections (7s+ for 350
|
|
451
|
+
* rows)". So the admin fetch avoided it and REST took it, and the fast,
|
|
452
|
+
* batched loader that the admin used was unreachable from the HTTP API.
|
|
453
|
+
*
|
|
454
|
+
* There is one loader now, {@link loadIncludes}, and every read reaches it:
|
|
455
|
+
* one query per relation per level, never one per row. That also makes this
|
|
456
|
+
* method and the realtime refetch the same code, which is what stops
|
|
457
|
+
* `find()` and `listen()` returning different shapes for one query.
|
|
329
458
|
*
|
|
330
|
-
* @param include -
|
|
459
|
+
* @param include - see {@link IncludeSpec}: names, dotted paths, `["*"]`,
|
|
460
|
+
* or the parametrised tree. An unknown name is a 400 `UNKNOWN_RELATION`.
|
|
331
461
|
*/
|
|
332
462
|
fetchCollectionForRest<M extends Record<string, unknown>>(collectionPath: string, options?: {
|
|
333
463
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
@@ -343,28 +473,38 @@ export declare class FetchService {
|
|
|
343
473
|
vectorSearch?: VectorSearchParams;
|
|
344
474
|
/** Narrow to the rows reachable from a parent through a relation. */
|
|
345
475
|
relatedTo?: NestedPathHop;
|
|
346
|
-
|
|
476
|
+
/**
|
|
477
|
+
* See `FetchCollectionProps.withDeleted`. Applied by
|
|
478
|
+
* `fetchRowsWithConditionsRaw` — the one place this pipeline builds
|
|
479
|
+
* a WHERE — so it holds for the listing, the nested-path listing
|
|
480
|
+
* and the realtime refetch alike.
|
|
481
|
+
*/
|
|
482
|
+
withDeleted?: WithDeleted;
|
|
483
|
+
/** Columns to read — a projection pushed into the SELECT. */
|
|
484
|
+
fields?: string[];
|
|
485
|
+
/** `SELECT DISTINCT` over the projection. */
|
|
486
|
+
distinct?: boolean;
|
|
487
|
+
/** Ask each row which declared search fields matched. */
|
|
488
|
+
searchExplain?: boolean;
|
|
489
|
+
}, include?: IncludeSpec): Promise<Record<string, unknown>[]>;
|
|
347
490
|
/**
|
|
348
|
-
* Fetch a single row with
|
|
349
|
-
|
|
350
|
-
|
|
491
|
+
* Fetch a single row, with the relations `include` names.
|
|
492
|
+
*
|
|
493
|
+
* The same two steps `fetchCollectionForRest` takes, for one row, through
|
|
494
|
+
* the same {@link loadIncludes}. It used to be a separate implementation on
|
|
495
|
+
* `db.query.findFirst({ with })` with a hand-written N+1 fallback beneath
|
|
496
|
+
* it, and the two disagreed: the primary path returned a to-one relation as
|
|
497
|
+
* the target's columns, the fallback merged an `id` over them, and a
|
|
498
|
+
* relation that resolved to nothing was *absent* on one path and `null` on
|
|
499
|
+
* the other. `find()[0]` and `findById()` now answer with the same row.
|
|
500
|
+
*/
|
|
501
|
+
fetchOneForRest<M extends Record<string, unknown>>(collectionPath: string, id: string | number, include?: IncludeSpec, databaseId?: string, options?: {
|
|
502
|
+
fields?: string[];
|
|
503
|
+
/** See `FetchCollectionProps.withDeleted`. */
|
|
504
|
+
withDeleted?: WithDeleted;
|
|
505
|
+
}): Promise<Record<string, unknown> | null>;
|
|
351
506
|
/**
|
|
352
507
|
* Fetch raw rows without any relation processing (for REST fast path)
|
|
353
508
|
*/
|
|
354
509
|
private fetchRowsWithConditionsRaw;
|
|
355
|
-
/**
|
|
356
|
-
* Check if the Drizzle instance has the relational query API available
|
|
357
|
-
* for a given collection path.
|
|
358
|
-
* Note: Primary path now uses inline `getQueryBuilder()` checks.
|
|
359
|
-
*/
|
|
360
|
-
private hasDrizzleQueryAPI;
|
|
361
|
-
/**
|
|
362
|
-
* Fallback path used when db.query is unavailable.
|
|
363
|
-
* The primary path uses db.query.findMany with `with` config, which
|
|
364
|
-
* loads all relations in a single query.
|
|
365
|
-
*
|
|
366
|
-
* Batch fetch many-to-many related rows for multiple parent IDs.
|
|
367
|
-
* Groups results by parent ID to avoid N+1.
|
|
368
|
-
*/
|
|
369
|
-
private batchFetchManyRelatedRows;
|
|
370
510
|
}
|
|
@@ -51,13 +51,21 @@ export declare class PersistService {
|
|
|
51
51
|
* Save an row (create or update)
|
|
52
52
|
*
|
|
53
53
|
* With `options.upsert`, the row is written with INSERT ... ON CONFLICT DO
|
|
54
|
-
* UPDATE
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* UPDATE rather than a plain UPDATE. That is one statement, so it cannot
|
|
55
|
+
* lose a race the way a read-then-write can, and it does not care whether
|
|
56
|
+
* the row already exists — which is what a re-runnable import needs. The
|
|
57
|
+
* conflict is matched on the primary key unless `options.onConflict` names
|
|
58
|
+
* other columns; see {@link SaveProps.onConflict} for why that matters.
|
|
59
|
+
*
|
|
60
|
+
* `values` may carry field operations (`{ views: { $inc: 1 } }`). They are
|
|
61
|
+
* split out here rather than at the REST boundary because every request
|
|
62
|
+
* boundary — HTTP, the WebSocket, in-process `context.data` — reaches this
|
|
63
|
+
* one method, and a rule applied at one door is a rule the other doors do
|
|
64
|
+
* not have. What they compile to is `field-op-sql.ts`.
|
|
58
65
|
*/
|
|
59
66
|
save<M extends Record<string, unknown>>(collectionPath: string, values: Partial<M>, id?: string | number, databaseId?: string, options?: {
|
|
60
67
|
upsert?: boolean;
|
|
68
|
+
onConflict?: readonly string[];
|
|
61
69
|
}): Promise<Record<string, unknown>>;
|
|
62
70
|
/**
|
|
63
71
|
* Get the RelationService instance for external use
|
|
@@ -76,6 +84,11 @@ export declare class PersistService {
|
|
|
76
84
|
getFetchService(): FetchService;
|
|
77
85
|
/**
|
|
78
86
|
* Translate raw PostgreSQL / Drizzle errors into user-friendly messages.
|
|
87
|
+
*
|
|
88
|
+
* `collection` is passed so the SQLSTATE can be turned into a *field*: the
|
|
89
|
+
* error carries a physical column and the caller only ever sees wire names,
|
|
90
|
+
* so `author_id` had to be translated to `authorId` by whoever read the
|
|
91
|
+
* message — which nothing does. See `pgFieldViolations`.
|
|
79
92
|
*/
|
|
80
93
|
private toUserFriendlyError;
|
|
81
94
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SQL } from "drizzle-orm";
|
|
1
2
|
import { DrizzleClient } from "../interfaces.js";
|
|
2
3
|
import { CollectionConfig, FilterValues, OrderByTuple, ResolvedRelation, ResolvedHasMany, ResolvedHasOne } from "@rebasepro/types";
|
|
3
4
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
|
|
@@ -136,13 +137,20 @@ export declare class RelationService {
|
|
|
136
137
|
*/
|
|
137
138
|
isRelated(hop: NestedPathHop, targetId: string | number): Promise<boolean>;
|
|
138
139
|
/**
|
|
139
|
-
* Batch fetch related rows for multiple parent rows to avoid N+1 queries
|
|
140
|
+
* Batch fetch related rows for multiple parent rows to avoid N+1 queries.
|
|
141
|
+
*
|
|
142
|
+
* `narrow` is an extra condition on the *target* table — what a per-include
|
|
143
|
+
* `where` compiles to. It is pushed into the query rather than applied to
|
|
144
|
+
* the rows that come back, because filtering afterwards reads every related
|
|
145
|
+
* row of every parent in order to discard most of them.
|
|
140
146
|
*/
|
|
141
|
-
batchFetchRelatedEntities(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation): Promise<Map<string, RelatedRow<Record<string, unknown>>>>;
|
|
147
|
+
batchFetchRelatedEntities(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation, narrow?: SQL): Promise<Map<string, RelatedRow<Record<string, unknown>>>>;
|
|
142
148
|
/**
|
|
143
149
|
* Batch fetch many-cardinality related rows for multiple parent rows.
|
|
144
150
|
* Returns a Map<parentId, RelatedRow[]> instead of Map<parentId, RelatedRow>.
|
|
145
151
|
* Uses a single SQL query with IN clause to avoid N+1.
|
|
146
152
|
*/
|
|
147
|
-
batchFetchRelatedEntitiesMany(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation
|
|
153
|
+
batchFetchRelatedEntitiesMany(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation,
|
|
154
|
+
/** An extra condition on the target — see {@link batchFetchRelatedEntities}. */
|
|
155
|
+
narrow?: SQL): Promise<Map<string, RelatedRow<Record<string, unknown>>[]>>;
|
|
148
156
|
}
|
|
@@ -38,6 +38,14 @@ export declare function getCollectionByPath(collectionPath: string, registry: Po
|
|
|
38
38
|
/**
|
|
39
39
|
* Reject a write naming something that is not a column of the table.
|
|
40
40
|
*
|
|
41
|
+
* "The table" is the one read back from `information_schema` at boot — see
|
|
42
|
+
* `catalogue-schema.ts` — so this asks the database, not a file. It used to ask
|
|
43
|
+
* the generated module, which made the commonest cause of this error a stale
|
|
44
|
+
* `schema.generated.ts`: a property added to a collection was created in the
|
|
45
|
+
* database by boot-ensure and still answered `400` on its first save, because
|
|
46
|
+
* the module had not been regenerated. That cause no longer exists, and the
|
|
47
|
+
* remedy below no longer names it.
|
|
48
|
+
*
|
|
41
49
|
* Drizzle builds INSERT from `Object.entries(table[Symbol.Columns])` and UPDATE
|
|
42
50
|
* from `Object.keys(tableColumns)`, so a key the table does not carry is not
|
|
43
51
|
* rejected by anything — it is *left out of the statement*. The insert answers
|
|
@@ -81,10 +89,16 @@ export declare function getTableForCollection(collection: CollectionConfig, regi
|
|
|
81
89
|
* The key columns a collection's rows are addressed by.
|
|
82
90
|
*
|
|
83
91
|
* Three tiers, in order: properties marked `isId`, the primary keys of the
|
|
84
|
-
*
|
|
85
|
-
* visible to the browser, which is why a key known only to
|
|
92
|
+
* registered table, and finally a column literally named `id`. Only the first is
|
|
93
|
+
* visible to the browser, which is why a key known only to the table is reported
|
|
86
94
|
* at boot — see {@link warnOnKeysTheAdminCannotResolve}.
|
|
87
95
|
*
|
|
96
|
+
* The second tier reads the table the registry holds, which is built from
|
|
97
|
+
* `information_schema` — the database's own primary key, not a generated file's
|
|
98
|
+
* claim about it. Composite keys are declared as a table constraint rather than
|
|
99
|
+
* on each column (that is the only legal way to say it), so both shapes are
|
|
100
|
+
* asked for.
|
|
101
|
+
*
|
|
88
102
|
* Returns `[]` when nothing resolves, rather than throwing. It used to open by
|
|
89
103
|
* resolving the table, which throws when there is none — so the `isId` tier,
|
|
90
104
|
* which needs no table at all, was unreachable for exactly the collections
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { FilterValues, LogicalCondition, OrderByTuple } from "@rebasepro/types";
|
|
1
|
+
import { FilterValues, IncludeSpec, LogicalCondition, OrderByTuple } from "@rebasepro/types";
|
|
2
2
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
3
3
|
import { FetchService } from "./FetchService.js";
|
|
4
|
+
import type { WithDeleted } from "./soft-delete.js";
|
|
4
5
|
import { PersistService } from "./PersistService.js";
|
|
5
6
|
import { RelationService } from "./RelationService.js";
|
|
6
7
|
import { DataRepository, DrizzleClient } from "../interfaces.js";
|
|
@@ -30,7 +31,9 @@ export declare class DataService implements DataRepository {
|
|
|
30
31
|
/**
|
|
31
32
|
* Fetch a single row by ID
|
|
32
33
|
*/
|
|
33
|
-
fetchOne<M extends Record<string, unknown>>(collectionPath: string, id: string | number, databaseId?: string
|
|
34
|
+
fetchOne<M extends Record<string, unknown>>(collectionPath: string, id: string | number, databaseId?: string,
|
|
35
|
+
/** See `FetchCollectionProps.withDeleted`. */
|
|
36
|
+
withDeleted?: WithDeleted): Promise<Record<string, unknown> | undefined>;
|
|
34
37
|
/**
|
|
35
38
|
* Fetch a collection of rows with optional filtering, ordering, and pagination
|
|
36
39
|
*/
|
|
@@ -46,7 +49,28 @@ export declare class DataService implements DataRepository {
|
|
|
46
49
|
searchString?: string;
|
|
47
50
|
databaseId?: string;
|
|
48
51
|
vectorSearch?: VectorSearchParams;
|
|
52
|
+
/** See `FetchCollectionProps.withDeleted`. */
|
|
53
|
+
withDeleted?: WithDeleted;
|
|
49
54
|
}): Promise<Record<string, unknown>[]>;
|
|
55
|
+
/**
|
|
56
|
+
* The REST read pipeline: flat rows, with exactly the relations `include`
|
|
57
|
+
* names — see {@link FetchService.fetchCollectionForRest}.
|
|
58
|
+
*
|
|
59
|
+
* Exposed on the facade because it is what *every* consumer should be
|
|
60
|
+
* reading through. The realtime refetch used to go via `fetchCollection`
|
|
61
|
+
* above, which nests a relation under a `{ __type: "relation" }` envelope
|
|
62
|
+
* and loaded every relation regardless, so a subscription and a REST list
|
|
63
|
+
* answered the same query with two different row shapes.
|
|
64
|
+
*/
|
|
65
|
+
fetchCollectionForRest<M extends Record<string, unknown>>(collectionPath: string, options?: Parameters<FetchService["fetchCollectionForRest"]>[1], include?: IncludeSpec): Promise<Record<string, unknown>[]>;
|
|
66
|
+
/** One row, through the same pipeline. See {@link FetchService.fetchOneForRest}. */
|
|
67
|
+
fetchOneForRest<M extends Record<string, unknown>>(collectionPath: string, id: string | number, include?: IncludeSpec, databaseId?: string, options?: {
|
|
68
|
+
fields?: string[];
|
|
69
|
+
/** See `FetchCollectionProps.withDeleted`. */
|
|
70
|
+
withDeleted?: WithDeleted;
|
|
71
|
+
}): Promise<Record<string, unknown> | null>;
|
|
72
|
+
/** See {@link FetchService.cursorFor}. */
|
|
73
|
+
cursorFor(collectionPath: string, row: Record<string, unknown>, orderBy?: OrderByTuple[]): string | undefined;
|
|
50
74
|
/**
|
|
51
75
|
* Search rows by text
|
|
52
76
|
*/
|
|
@@ -71,6 +95,8 @@ export declare class DataService implements DataRepository {
|
|
|
71
95
|
databaseId?: string;
|
|
72
96
|
/** Only the `threshold` narrows the count — see `FetchService.count`. */
|
|
73
97
|
vectorSearch?: VectorSearchParams;
|
|
98
|
+
/** See `FetchCollectionProps.withDeleted`. */
|
|
99
|
+
withDeleted?: WithDeleted;
|
|
74
100
|
}): Promise<number>;
|
|
75
101
|
/**
|
|
76
102
|
* Check if a field value is unique in a collection
|
|
@@ -93,6 +119,7 @@ export declare class DataService implements DataRepository {
|
|
|
93
119
|
*/
|
|
94
120
|
save<M extends Record<string, unknown>>(collectionPath: string, values: Partial<M>, id?: string | number, databaseId?: string, options?: {
|
|
95
121
|
upsert?: boolean;
|
|
122
|
+
onConflict?: readonly string[];
|
|
96
123
|
}): Promise<Record<string, unknown>>;
|
|
97
124
|
/**
|
|
98
125
|
* Delete an row by ID
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SQL } from "drizzle-orm";
|
|
2
|
+
import { PgTable } from "drizzle-orm/pg-core";
|
|
3
|
+
import type { ParsedFieldOp } from "@rebasepro/server";
|
|
4
|
+
/**
|
|
5
|
+
* The expression one operation compiles to, or a 400 saying why it cannot.
|
|
6
|
+
*
|
|
7
|
+
* `field` is the property key; it is already known to be a column of `table`
|
|
8
|
+
* (the caller checks that against the same table it builds the statement from).
|
|
9
|
+
*/
|
|
10
|
+
export declare function compileFieldOp(table: PgTable, field: string, op: ParsedFieldOp, context: {
|
|
11
|
+
collectionPath: string;
|
|
12
|
+
}): SQL;
|
|
13
|
+
/** Compile a whole map of operations into a `SET`-ready object. */
|
|
14
|
+
export declare function compileFieldOps(table: PgTable, fieldOps: Record<string, ParsedFieldOp>, context: {
|
|
15
|
+
collectionPath: string;
|
|
16
|
+
}): Record<string, SQL>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FieldViewer } from "@rebasepro/common";
|
|
2
|
+
/**
|
|
3
|
+
* Run `fn` with `viewer` as the caller every row strip judges against.
|
|
4
|
+
*
|
|
5
|
+
* Nested calls shadow: a `dataAsAdmin` read inside a user request runs under the
|
|
6
|
+
* service identity for its duration and the user's viewer is restored after it,
|
|
7
|
+
* which is the behaviour both halves need and the reason this is not a variable.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withFieldViewer<T>(viewer: FieldViewer | undefined, fn: () => T): T;
|
|
10
|
+
/**
|
|
11
|
+
* The caller the current read is for, or `undefined` on the trusted server plane
|
|
12
|
+
* — an in-process `rebase.data` call on the base driver, a migration, the auth
|
|
13
|
+
* adapter reading a password hash to verify it.
|
|
14
|
+
*
|
|
15
|
+
* Absence cannot be reached from outside: every API ingress scopes its driver
|
|
16
|
+
* with `withAuth`, including an unauthenticated one (`roles: ["anon"]`), and
|
|
17
|
+
* that is what establishes the viewer.
|
|
18
|
+
*/
|
|
19
|
+
export declare function currentFieldViewer(): FieldViewer | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebSocket } from "ws";
|
|
2
2
|
import { EventEmitter } from "events";
|
|
3
|
-
import { DataDriver, WebSocketMessage, LogicalCondition, OrderByTuple } from "@rebasepro/types";
|
|
3
|
+
import { DataDriver, IncludeSpec, WebSocketMessage, LogicalCondition, OrderByTuple } from "@rebasepro/types";
|
|
4
4
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
5
5
|
import { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from "../interfaces.js";
|
|
6
6
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
|
|
@@ -21,6 +21,15 @@ export interface SubscriptionAuthContext {
|
|
|
21
21
|
* makes a realtime leak invisible from the surface people test.
|
|
22
22
|
*/
|
|
23
23
|
isAnonymous?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The custom claims on the socket's token, for the same reason
|
|
26
|
+
* `isAnonymous` is here: a refetch has to evaluate the same policies
|
|
27
|
+
* against the same principal the initial fetch did. A tenancy policy reads
|
|
28
|
+
* a claim, so a subscription that carried none would answer every frame
|
|
29
|
+
* from the tenant of nobody — which is no rows, silently, on a collection
|
|
30
|
+
* whose first page loaded fine.
|
|
31
|
+
*/
|
|
32
|
+
claims?: Record<string, unknown>;
|
|
24
33
|
}
|
|
25
34
|
/** What a channel frame is asking to do. */
|
|
26
35
|
export type ChannelAction = "join" | "broadcast" | "presence" | "history";
|
|
@@ -72,6 +81,20 @@ type StoredCollectionRequest = {
|
|
|
72
81
|
searchString?: string;
|
|
73
82
|
/** Ask each row which declared search field matched — populates `_matches`. */
|
|
74
83
|
searchExplain?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Relations to load, exactly as a REST list would.
|
|
86
|
+
*
|
|
87
|
+
* A subscription could not name any, and the refetch loaded **every**
|
|
88
|
+
* relation because it passed none — so `find()` returned a row with a
|
|
89
|
+
* foreign key and `listen()` returned the same row with a nested object
|
|
90
|
+
* where that key was. Same query, two shapes, and a client rendering both
|
|
91
|
+
* saw the row change the moment a write landed.
|
|
92
|
+
*/
|
|
93
|
+
include?: IncludeSpec;
|
|
94
|
+
/** Columns to return — the same projection `?fields=` asks for. */
|
|
95
|
+
fields?: string[];
|
|
96
|
+
/** `SELECT DISTINCT` over the projection. */
|
|
97
|
+
distinct?: boolean;
|
|
75
98
|
};
|
|
76
99
|
/**
|
|
77
100
|
* A registered subscription, plus the two counters that order its deliveries.
|
|
@@ -356,6 +379,17 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
356
379
|
* with set_config calls so PostgreSQL RLS policies are enforced.
|
|
357
380
|
*/
|
|
358
381
|
private fetchCollectionWithAuth;
|
|
382
|
+
/**
|
|
383
|
+
* The `meta` a `collection_update` frame carries beside its rows.
|
|
384
|
+
*
|
|
385
|
+
* Frames used to carry rows and primary keys and nothing else, so the
|
|
386
|
+
* client issued a `GET /count` **per push** to fill in a total it needed to
|
|
387
|
+
* render the same list it had just been handed — one extra round trip per
|
|
388
|
+
* write, per subscriber, forever. The refetch already knows the query; it
|
|
389
|
+
* counts once, here, under the same RLS transaction that read the rows, so
|
|
390
|
+
* the total describes the same set they came from.
|
|
391
|
+
*/
|
|
392
|
+
private collectionMetaWithAuth;
|
|
359
393
|
/**
|
|
360
394
|
* Debounce an row refetch for a WebSocket subscription.
|
|
361
395
|
*/
|
|
@@ -29,17 +29,41 @@ export type RelationStyle = "ref" | "inline";
|
|
|
29
29
|
* to unwrap that same level back out.
|
|
30
30
|
*/
|
|
31
31
|
export declare function isJunctionRelation(relation: ResolvedRelation): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Apply {@link toRestScalar} across a row, leaving undeclared columns alone.
|
|
34
|
+
*
|
|
35
|
+
* Exported because the include loader attaches related rows itself, and a
|
|
36
|
+
* target rendered differently from its parent is the shape bug this whole file
|
|
37
|
+
* exists to prevent — a date was a string at the top level and a
|
|
38
|
+
* `{ __type: "date" }` envelope one level down, in the same response.
|
|
39
|
+
*/
|
|
40
|
+
export declare function toRestValues(row: Record<string, unknown>, collection: CollectionConfig): Record<string, unknown>;
|
|
32
41
|
/** Render one target row in the requested style. */
|
|
33
42
|
/**
|
|
34
|
-
* Drop every column
|
|
43
|
+
* Drop every column this caller may not read.
|
|
35
44
|
*
|
|
36
45
|
* Password hashes and verification tokens have to be readable server-side but
|
|
37
46
|
* must never reach a client — and "never" has to mean every exit from this
|
|
38
47
|
* pipeline, including relation targets, or a secret leaks through whichever
|
|
39
48
|
* path was overlooked. Keyed by both the property name and its column name,
|
|
40
49
|
* since a row can arrive keyed either way depending on the caller.
|
|
50
|
+
*
|
|
51
|
+
* `excludeFromApi` is the case with no roles in it: `effectiveAccess` expands
|
|
52
|
+
* the flag to `read: []`, which no caller satisfies, so the flag needs no branch
|
|
53
|
+
* of its own here and no longer has one. `access.read: ["hr"]` is the same walk
|
|
54
|
+
* asking a different question of {@link currentFieldViewer}.
|
|
55
|
+
*
|
|
56
|
+
* The field is **deleted**, never nulled. A withheld value that arrives as
|
|
57
|
+
* `null` is indistinguishable from a stored `null`, which turns a permission
|
|
58
|
+
* boundary into a question a client can answer by counting nulls — and it makes
|
|
59
|
+
* an `update` that echoes the row back overwrite the real value with the null it
|
|
60
|
+
* was handed.
|
|
61
|
+
*
|
|
62
|
+
* `_matches` is filtered rather than deleted: it is the list of fields a text
|
|
63
|
+
* search hit, and a field the caller cannot read must not appear in it even
|
|
64
|
+
* though the array itself is theirs to see.
|
|
41
65
|
*/
|
|
42
|
-
export declare function
|
|
66
|
+
export declare function stripUnreadable(row: Record<string, unknown>, collection: CollectionConfig): Record<string, unknown>;
|
|
43
67
|
/**
|
|
44
68
|
* The address a relation ref points at.
|
|
45
69
|
*
|
|
@@ -65,9 +89,9 @@ export declare function toFlatRow(row: Record<string, unknown>, collection: Coll
|
|
|
65
89
|
*
|
|
66
90
|
* Values are the ones the database returned, except where that contradicts the
|
|
67
91
|
* declared type: a `number` property is served as a number (see
|
|
68
|
-
* {@link coerceDeclaredNumber})
|
|
69
|
-
*
|
|
70
|
-
*
|
|
92
|
+
* {@link coerceDeclaredNumber}) and a `date` as RFC 3339, which is what this
|
|
93
|
+
* server's own OpenAPI document says a date column is (see
|
|
94
|
+
* {@link toRestDate}).
|
|
71
95
|
*
|
|
72
96
|
* Keyed by the row rather than by the relation list — a REST fetch only loads
|
|
73
97
|
* the relations `include` asked for, so the row is the authority on which are
|