@rebasepro/common 0.21.2-canary.g1ea48be → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collections/default-collections.d.ts +1 -1
- package/dist/collections/engine-support.d.ts +23 -0
- package/dist/collections/field-access.d.ts +5 -0
- package/dist/collections/index.d.ts +2 -0
- package/dist/collections/soft-delete.d.ts +23 -0
- package/dist/data/filter-dialect.d.ts +13 -0
- package/dist/index.es.js +281 -46
- package/dist/index.es.js.map +1 -1
- package/dist/util/policy/index.d.ts +1 -0
- package/dist/util/policy/policyToSecurityRule.d.ts +27 -0
- package/package.json +3 -3
- package/dist/util/callback-errors.test.d.ts +0 -1
- package/dist/util/internal-tables.test.d.ts +0 -1
- package/dist/util/permissions.test.d.ts +0 -1
- package/dist/util/table-name.test.d.ts +0 -1
|
@@ -17,8 +17,8 @@ export declare const defaultUsersCollection: import("@rebasepro/types").Postgres
|
|
|
17
17
|
} & {
|
|
18
18
|
id?: string | undefined;
|
|
19
19
|
metadata?: import("@rebasepro/types").InferEntityType<{}> | undefined;
|
|
20
|
-
photoURL?: string | undefined;
|
|
21
20
|
roles?: string[] | undefined;
|
|
21
|
+
photoURL?: string | undefined;
|
|
22
22
|
passwordHash?: string | undefined;
|
|
23
23
|
emailVerified?: boolean | undefined;
|
|
24
24
|
emailVerificationToken?: string | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* Refuse a `beforeQuery` hook on a collection Postgres does not serve.
|
|
4
|
+
*
|
|
5
|
+
* `beforeQuery` narrows a read *before it is compiled*, which means compiling
|
|
6
|
+
* a declarative filter into the query the engine is about to run. Only
|
|
7
|
+
* `@rebasepro/server-postgres` does that today. On any other engine the hook
|
|
8
|
+
* would look configured and do nothing — and for a hook whose whole purpose is
|
|
9
|
+
* to restrict which rows come back, "nothing" means every row served to
|
|
10
|
+
* everybody.
|
|
11
|
+
*
|
|
12
|
+
* So it is a boot failure, named. The alternative — declaring the hook only on
|
|
13
|
+
* `PostgresCollectionConfig`, the way `search` is — is not available:
|
|
14
|
+
* `callbacks` is one engine-agnostic type shared by every collection, and
|
|
15
|
+
* splitting it would fork `afterRead` and the five write hooks along with it.
|
|
16
|
+
*
|
|
17
|
+
* Lives here rather than in either driver because both have to run it: the
|
|
18
|
+
* Postgres schema planner sees every collection in a mixed app, and a
|
|
19
|
+
* Mongo-only app never boots a Postgres planner at all.
|
|
20
|
+
*
|
|
21
|
+
* Called with *every* collection, before an engine filters any out.
|
|
22
|
+
*/
|
|
23
|
+
export declare const assertBeforeQueryIsPostgresOnly: (collections: CollectionConfig[]) => void;
|
|
@@ -60,6 +60,11 @@ export declare function canWriteField(property: Property | undefined, viewer: Fi
|
|
|
60
60
|
* who knows the table can send `password_hash` as readily as `passwordHash`, and
|
|
61
61
|
* a rule that only knew the wire name would be one rename away from useless.
|
|
62
62
|
*
|
|
63
|
+
* A restricted to-one relation takes its foreign key with it. `bandId: 7` names
|
|
64
|
+
* the band exactly as `band: { id: 7 }` does, and the row carries the column
|
|
65
|
+
* beside the relation — so a rule on the relation that left the key alone
|
|
66
|
+
* withheld the value from the row and still let `?bandId=7` ask for it.
|
|
67
|
+
*
|
|
63
68
|
* `kind` picks which half of the rule is read; nothing else differs.
|
|
64
69
|
*/
|
|
65
70
|
export declare function restrictedFieldNames(collection: CollectionConfig, viewer: FieldViewer | undefined, kind: "read" | "write"): {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
/** The field `softDelete: true` means, when the object form names none. */
|
|
3
|
+
export declare const DEFAULT_SOFT_DELETE_FIELD = "deletedAt";
|
|
4
|
+
/**
|
|
5
|
+
* The property a `softDelete` collection records a deletion in — its wire
|
|
6
|
+
* name, what a delete stamps and what a restore (an update to `null`) clears —
|
|
7
|
+
* or `undefined` for a collection without soft delete.
|
|
8
|
+
*
|
|
9
|
+
* One reading of the declaration for every layer that needs it: the driver
|
|
10
|
+
* that stamps and filters on it, and the REST route that has to recognise a
|
|
11
|
+
* restore before it looks the row up.
|
|
12
|
+
*/
|
|
13
|
+
export declare function softDeleteFieldOf(collection: CollectionConfig | undefined): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Whether an update's values restore a soft-deleted row: they set the
|
|
16
|
+
* soft-delete field back to `null`, the documented restore.
|
|
17
|
+
*
|
|
18
|
+
* A restore is an ordinary update of a row every default read hides, so a door
|
|
19
|
+
* that looks the row up before updating it has to include the trashed rows for
|
|
20
|
+
* this update — and only for this one: any other edit of a trashed row is a
|
|
21
|
+
* 404.
|
|
22
|
+
*/
|
|
23
|
+
export declare function restoresSoftDeletedRow(collection: CollectionConfig | undefined, values: Record<string, unknown> | undefined): boolean;
|
|
@@ -64,6 +64,19 @@ export declare class UnknownFilterOperatorError extends Error {
|
|
|
64
64
|
};
|
|
65
65
|
constructor(field: string, operator: string);
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The query-parameter names a REST listing reads as something other than a
|
|
69
|
+
* field filter: pagination, sorting, includes, projection, search, the logical
|
|
70
|
+
* groups, the JSON `where` dialect, an aggregate's `select`/`groupBy`, and the
|
|
71
|
+
* soft-delete switches.
|
|
72
|
+
*
|
|
73
|
+
* One list for both ends. The server skips these names when it collects
|
|
74
|
+
* `?<field>=<op>.<value>` filters, and the SDK sends a filter on a column with
|
|
75
|
+
* one of these names inside `?where=` instead — a collection is free to have a
|
|
76
|
+
* `page`, `select` or `hard` column, and a filter on it sent as its own
|
|
77
|
+
* parameter was read as the parameter and never applied.
|
|
78
|
+
*/
|
|
79
|
+
export declare const RESERVED_QUERY_KEYS: ReadonlySet<string>;
|
|
67
80
|
/**
|
|
68
81
|
* Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
|
|
69
82
|
* querystring record.
|
package/dist/index.es.js
CHANGED
|
@@ -2130,6 +2130,81 @@ function evaluateCompare(op, left, right, ctx) {
|
|
|
2130
2130
|
return "unknown";
|
|
2131
2131
|
}
|
|
2132
2132
|
//#endregion
|
|
2133
|
+
//#region src/util/policy/policyToSecurityRule.ts
|
|
2134
|
+
/**
|
|
2135
|
+
* The rule a Postgres policy compiles back from — a `pg_policies` row, or what
|
|
2136
|
+
* a policy editor produced in that shape.
|
|
2137
|
+
*
|
|
2138
|
+
* One definition for every place that turns a policy into a rule: Studio's RLS
|
|
2139
|
+
* editor ("Save" and "Import to codebase"), the collection editor's RLS tab
|
|
2140
|
+
* ("Import to codebase") and "Import from table"
|
|
2141
|
+
* (`buildCollectionFromTableMetadata`). The rule is what `db push` compiles back
|
|
2142
|
+
* into the database, so anything this gets wrong is a policy that changes on
|
|
2143
|
+
* the way back:
|
|
2144
|
+
*
|
|
2145
|
+
* - The `TO` list names *database* roles, which is `pgRoles`. `roles` holds
|
|
2146
|
+
* *application* roles and compiles to a `rebase.roles()` check: filed there,
|
|
2147
|
+
* `TO public` is a check no user passes, and on a restrictive policy — which
|
|
2148
|
+
* compiles to `NOT (roles) OR condition` — a gate every user passes.
|
|
2149
|
+
* `pgRoles` is omitted at the `public` default, so a rule that targets every
|
|
2150
|
+
* connection, nearly all of them, carries no advanced field it does not need.
|
|
2151
|
+
* - The WITH CHECK is kept whether or not there is a USING beside it. Every
|
|
2152
|
+
* INSERT policy has only a check, and a rule with neither clause compiles to
|
|
2153
|
+
* `WITH CHECK (false)`, which stops every insert.
|
|
2154
|
+
* - `mode` is written when the policy states one: a restrictive policy read
|
|
2155
|
+
* back as permissive is OR'd with every grant beside it.
|
|
2156
|
+
* - Retired RLS helpers (`auth.uid()`) are respelled, as `sqlToPolicy` does
|
|
2157
|
+
* for what it reads, because this rule is written into the project's config.
|
|
2158
|
+
*/
|
|
2159
|
+
function policyToSecurityRule(policy) {
|
|
2160
|
+
const operation = toSecurityOperation(policy.cmd);
|
|
2161
|
+
const mode = toSecurityMode(policy.permissive);
|
|
2162
|
+
const pgRoles = toPgRoles(policy.roles);
|
|
2163
|
+
const base = {
|
|
2164
|
+
...policy.policyname ? { name: policy.policyname } : {},
|
|
2165
|
+
...operation ? { operation } : {},
|
|
2166
|
+
...mode ? { mode } : {},
|
|
2167
|
+
...pgRoles ? { pgRoles } : {}
|
|
2168
|
+
};
|
|
2169
|
+
const using = policy.qual ? rewriteLegacyRlsFunctions(policy.qual) : void 0;
|
|
2170
|
+
const withCheck = policy.with_check ? rewriteLegacyRlsFunctions(policy.with_check) : void 0;
|
|
2171
|
+
if (using) return withCheck ? {
|
|
2172
|
+
...base,
|
|
2173
|
+
using,
|
|
2174
|
+
withCheck
|
|
2175
|
+
} : {
|
|
2176
|
+
...base,
|
|
2177
|
+
using
|
|
2178
|
+
};
|
|
2179
|
+
if (withCheck) return {
|
|
2180
|
+
...base,
|
|
2181
|
+
withCheck
|
|
2182
|
+
};
|
|
2183
|
+
return base;
|
|
2184
|
+
}
|
|
2185
|
+
function toPgRoles(roles) {
|
|
2186
|
+
if (!roles || roles.length === 0) return void 0;
|
|
2187
|
+
if (roles.length === 1 && roles[0] === "public") return void 0;
|
|
2188
|
+
return [...roles];
|
|
2189
|
+
}
|
|
2190
|
+
function toSecurityOperation(cmd) {
|
|
2191
|
+
switch (cmd) {
|
|
2192
|
+
case "SELECT": return "select";
|
|
2193
|
+
case "INSERT": return "insert";
|
|
2194
|
+
case "UPDATE": return "update";
|
|
2195
|
+
case "DELETE": return "delete";
|
|
2196
|
+
case "ALL": return "all";
|
|
2197
|
+
default: return;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
function toSecurityMode(permissive) {
|
|
2201
|
+
switch (permissive) {
|
|
2202
|
+
case "PERMISSIVE": return "permissive";
|
|
2203
|
+
case "RESTRICTIVE": return "restrictive";
|
|
2204
|
+
default: return;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
//#endregion
|
|
2133
2208
|
//#region src/util/permissions.ts
|
|
2134
2209
|
/** Combine clause results with AND under three-valued (Kleene) logic. */
|
|
2135
2210
|
function kleeneAnd(values) {
|
|
@@ -3293,6 +3368,26 @@ function buildConditionContext(params) {
|
|
|
3293
3368
|
*/
|
|
3294
3369
|
//#endregion
|
|
3295
3370
|
//#region src/util/pg-column-to-property.ts
|
|
3371
|
+
var POLICY_COMMANDS = [
|
|
3372
|
+
"SELECT",
|
|
3373
|
+
"INSERT",
|
|
3374
|
+
"UPDATE",
|
|
3375
|
+
"DELETE",
|
|
3376
|
+
"ALL"
|
|
3377
|
+
];
|
|
3378
|
+
/**
|
|
3379
|
+
* A policy's command, as `pg_policies.cmd` names it.
|
|
3380
|
+
*
|
|
3381
|
+
* Anything else is refused rather than guessed at. A rule with no operation
|
|
3382
|
+
* compiles to FOR ALL, where the USING doubles as the WITH CHECK — so a
|
|
3383
|
+
* `SELECT ... USING (true)` whose command went unrecognised (`pg_policy.polcmd`
|
|
3384
|
+
* spells it `r`) would come back a write grant.
|
|
3385
|
+
*/
|
|
3386
|
+
function policyCommand(policy, tableName) {
|
|
3387
|
+
const cmd = POLICY_COMMANDS.find((c) => c === policy.cmd);
|
|
3388
|
+
if (!cmd) throw new Error(`Policy "${policy.policy_name}" on "${tableName}" has the command "${policy.cmd}", not one of ${POLICY_COMMANDS.join(", ")}, so it cannot be imported as a security rule.`);
|
|
3389
|
+
return cmd;
|
|
3390
|
+
}
|
|
3296
3391
|
/**
|
|
3297
3392
|
* Maps a PostgreSQL column data type to a Rebase property type.
|
|
3298
3393
|
*/
|
|
@@ -3490,40 +3585,14 @@ function buildCollectionFromTableMetadata(tableName, metadata) {
|
|
|
3490
3585
|
}
|
|
3491
3586
|
});
|
|
3492
3587
|
}
|
|
3493
|
-
if (metadata.policies) for (const policy of metadata.policies) {
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
break;
|
|
3502
|
-
case "INSERT":
|
|
3503
|
-
operations = ["insert"];
|
|
3504
|
-
break;
|
|
3505
|
-
case "UPDATE":
|
|
3506
|
-
operations = ["update"];
|
|
3507
|
-
break;
|
|
3508
|
-
case "DELETE":
|
|
3509
|
-
operations = ["delete"];
|
|
3510
|
-
break;
|
|
3511
|
-
}
|
|
3512
|
-
const qual = policy.qual ? rewriteLegacyRlsFunctions(policy.qual) : void 0;
|
|
3513
|
-
const withCheck = policy.with_check ? rewriteLegacyRlsFunctions(policy.with_check) : void 0;
|
|
3514
|
-
if (qual) securityRules.push({
|
|
3515
|
-
name: policy.policy_name,
|
|
3516
|
-
operations,
|
|
3517
|
-
roles: policy.roles ?? [],
|
|
3518
|
-
using: qual,
|
|
3519
|
-
...withCheck ? { withCheck } : {}
|
|
3520
|
-
});
|
|
3521
|
-
else securityRules.push({
|
|
3522
|
-
name: policy.policy_name,
|
|
3523
|
-
operations,
|
|
3524
|
-
roles: policy.roles ?? []
|
|
3525
|
-
});
|
|
3526
|
-
}
|
|
3588
|
+
if (metadata.policies) for (const policy of metadata.policies) securityRules.push(policyToSecurityRule({
|
|
3589
|
+
policyname: policy.policy_name,
|
|
3590
|
+
cmd: policyCommand(policy, tableName),
|
|
3591
|
+
permissive: policy.permissive,
|
|
3592
|
+
roles: policy.roles,
|
|
3593
|
+
qual: policy.qual ?? null,
|
|
3594
|
+
with_check: policy.with_check ?? null
|
|
3595
|
+
}));
|
|
3527
3596
|
return {
|
|
3528
3597
|
name: prettifyIdentifier(tableName),
|
|
3529
3598
|
slug: tableName,
|
|
@@ -3662,6 +3731,7 @@ var REBASE_INTERNAL_TABLES = [
|
|
|
3662
3731
|
"api_keys",
|
|
3663
3732
|
"cron_logs",
|
|
3664
3733
|
"cron_claims",
|
|
3734
|
+
"cron_job_state",
|
|
3665
3735
|
"jobs",
|
|
3666
3736
|
"rate_limit_hits",
|
|
3667
3737
|
"idempotency_keys",
|
|
@@ -4265,6 +4335,37 @@ var CollectionRegistry = class {
|
|
|
4265
4335
|
}
|
|
4266
4336
|
};
|
|
4267
4337
|
//#endregion
|
|
4338
|
+
//#region src/collections/engine-support.ts
|
|
4339
|
+
/**
|
|
4340
|
+
* Refuse a `beforeQuery` hook on a collection Postgres does not serve.
|
|
4341
|
+
*
|
|
4342
|
+
* `beforeQuery` narrows a read *before it is compiled*, which means compiling
|
|
4343
|
+
* a declarative filter into the query the engine is about to run. Only
|
|
4344
|
+
* `@rebasepro/server-postgres` does that today. On any other engine the hook
|
|
4345
|
+
* would look configured and do nothing — and for a hook whose whole purpose is
|
|
4346
|
+
* to restrict which rows come back, "nothing" means every row served to
|
|
4347
|
+
* everybody.
|
|
4348
|
+
*
|
|
4349
|
+
* So it is a boot failure, named. The alternative — declaring the hook only on
|
|
4350
|
+
* `PostgresCollectionConfig`, the way `search` is — is not available:
|
|
4351
|
+
* `callbacks` is one engine-agnostic type shared by every collection, and
|
|
4352
|
+
* splitting it would fork `afterRead` and the five write hooks along with it.
|
|
4353
|
+
*
|
|
4354
|
+
* Lives here rather than in either driver because both have to run it: the
|
|
4355
|
+
* Postgres schema planner sees every collection in a mixed app, and a
|
|
4356
|
+
* Mongo-only app never boots a Postgres planner at all.
|
|
4357
|
+
*
|
|
4358
|
+
* Called with *every* collection, before an engine filters any out.
|
|
4359
|
+
*/
|
|
4360
|
+
var assertBeforeQueryIsPostgresOnly = (collections) => {
|
|
4361
|
+
for (const collection of collections) {
|
|
4362
|
+
if (isPostgresCollectionConfig(collection)) continue;
|
|
4363
|
+
if (!collection.callbacks?.beforeQuery) continue;
|
|
4364
|
+
const engine = collection.engine ?? "non-postgres";
|
|
4365
|
+
throw new Error(`${collection.slug}.callbacks.beforeQuery: narrowing a read before it is compiled is a Postgres feature, and this collection is served by \`${engine}\`. Remove the hook — it would otherwise look configured while every read kept returning every row the engine allows. Redaction that works on every engine is \`afterRead\`.`);
|
|
4366
|
+
}
|
|
4367
|
+
};
|
|
4368
|
+
//#endregion
|
|
4268
4369
|
//#region src/collections/field-access.ts
|
|
4269
4370
|
/**
|
|
4270
4371
|
* The role that satisfies any non-empty list.
|
|
@@ -4337,18 +4438,36 @@ function canWriteField(property, viewer) {
|
|
|
4337
4438
|
* who knows the table can send `password_hash` as readily as `passwordHash`, and
|
|
4338
4439
|
* a rule that only knew the wire name would be one rename away from useless.
|
|
4339
4440
|
*
|
|
4441
|
+
* A restricted to-one relation takes its foreign key with it. `bandId: 7` names
|
|
4442
|
+
* the band exactly as `band: { id: 7 }` does, and the row carries the column
|
|
4443
|
+
* beside the relation — so a rule on the relation that left the key alone
|
|
4444
|
+
* withheld the value from the row and still let `?bandId=7` ask for it.
|
|
4445
|
+
*
|
|
4340
4446
|
* `kind` picks which half of the rule is read; nothing else differs.
|
|
4341
4447
|
*/
|
|
4342
4448
|
function restrictedFieldNames(collection, viewer, kind) {
|
|
4343
4449
|
const declared = [];
|
|
4344
4450
|
const refused = /* @__PURE__ */ new Set();
|
|
4345
4451
|
const allowed = kind === "read" ? canReadField : canWriteField;
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4452
|
+
const properties = collection.properties ?? {};
|
|
4453
|
+
let relations;
|
|
4454
|
+
const restrict = (name) => {
|
|
4455
|
+
if (name in properties && !declared.includes(name)) declared.push(name);
|
|
4349
4456
|
refused.add(name);
|
|
4457
|
+
};
|
|
4458
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
4459
|
+
if (allowed(property, viewer)) continue;
|
|
4460
|
+
restrict(name);
|
|
4350
4461
|
const columnName = property.columnName;
|
|
4351
4462
|
if (columnName) refused.add(columnName);
|
|
4463
|
+
if (property.type === "relation") {
|
|
4464
|
+
relations ??= resolveCollectionRelations(collection);
|
|
4465
|
+
const relation = findRelation(relations, name);
|
|
4466
|
+
if (relation?.kind === "belongsTo") {
|
|
4467
|
+
restrict(fieldKeyForColumn(collection, relation.localKey));
|
|
4468
|
+
refused.add(relation.localKey);
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4352
4471
|
}
|
|
4353
4472
|
return {
|
|
4354
4473
|
declared,
|
|
@@ -4367,6 +4486,37 @@ function hasFieldAccessRules(collection) {
|
|
|
4367
4486
|
return false;
|
|
4368
4487
|
}
|
|
4369
4488
|
//#endregion
|
|
4489
|
+
//#region src/collections/soft-delete.ts
|
|
4490
|
+
/** The field `softDelete: true` means, when the object form names none. */
|
|
4491
|
+
var DEFAULT_SOFT_DELETE_FIELD = "deletedAt";
|
|
4492
|
+
/**
|
|
4493
|
+
* The property a `softDelete` collection records a deletion in — its wire
|
|
4494
|
+
* name, what a delete stamps and what a restore (an update to `null`) clears —
|
|
4495
|
+
* or `undefined` for a collection without soft delete.
|
|
4496
|
+
*
|
|
4497
|
+
* One reading of the declaration for every layer that needs it: the driver
|
|
4498
|
+
* that stamps and filters on it, and the REST route that has to recognise a
|
|
4499
|
+
* restore before it looks the row up.
|
|
4500
|
+
*/
|
|
4501
|
+
function softDeleteFieldOf(collection) {
|
|
4502
|
+
const declared = collection && "softDelete" in collection ? collection.softDelete : void 0;
|
|
4503
|
+
if (!declared) return void 0;
|
|
4504
|
+
return typeof declared === "object" && declared.field || "deletedAt";
|
|
4505
|
+
}
|
|
4506
|
+
/**
|
|
4507
|
+
* Whether an update's values restore a soft-deleted row: they set the
|
|
4508
|
+
* soft-delete field back to `null`, the documented restore.
|
|
4509
|
+
*
|
|
4510
|
+
* A restore is an ordinary update of a row every default read hides, so a door
|
|
4511
|
+
* that looks the row up before updating it has to include the trashed rows for
|
|
4512
|
+
* this update — and only for this one: any other edit of a trashed row is a
|
|
4513
|
+
* 404.
|
|
4514
|
+
*/
|
|
4515
|
+
function restoresSoftDeletedRow(collection, values) {
|
|
4516
|
+
const field = softDeleteFieldOf(collection);
|
|
4517
|
+
return field !== void 0 && values?.[field] === null;
|
|
4518
|
+
}
|
|
4519
|
+
//#endregion
|
|
4370
4520
|
//#region src/data/cursor.ts
|
|
4371
4521
|
/** A cursor that cannot be read at all — truncated, re-encoded, or invented. */
|
|
4372
4522
|
var CursorError = class CursorError extends Error {
|
|
@@ -5264,19 +5414,23 @@ function normalizeMaxRows(raw) {
|
|
|
5264
5414
|
* @param label the collection name, so an error says which walk failed
|
|
5265
5415
|
*/
|
|
5266
5416
|
async function* paginateFind(find, params, label = "collection") {
|
|
5267
|
-
const { pageSize, cursor, maxPages, ...rest } = params ?? {};
|
|
5417
|
+
const { pageSize, cursor, maxPages, after: startAfter, offset: startOffset, page: startPage, ...rest } = params ?? {};
|
|
5268
5418
|
const findParams = { ...rest };
|
|
5269
5419
|
const size = normalizePageSize(pageSize);
|
|
5270
5420
|
const pageCap = normalizeMaxPages(maxPages);
|
|
5271
|
-
const seekRequested = cursor !== void 0 && cursor !== null;
|
|
5272
|
-
if (
|
|
5421
|
+
const seekRequested = cursor !== void 0 && cursor !== null || !!startAfter;
|
|
5422
|
+
if (cursor !== void 0 && cursor !== null) {
|
|
5273
5423
|
const field = typeof cursor === "string" ? cursor : cursor.field;
|
|
5274
5424
|
const requested = typeof cursor === "object" && cursor !== null ? cursor.direction : void 0;
|
|
5275
5425
|
if (!normalizeOrderBy(findParams.orderBy)) findParams.orderBy = [field, requested ?? "asc"];
|
|
5276
5426
|
}
|
|
5277
|
-
let offset =
|
|
5427
|
+
let offset = resolveFindWindow({
|
|
5428
|
+
limit: size,
|
|
5429
|
+
offset: startOffset,
|
|
5430
|
+
page: startPage
|
|
5431
|
+
}).offset;
|
|
5278
5432
|
let pages = 0;
|
|
5279
|
-
let after;
|
|
5433
|
+
let after = startAfter || void 0;
|
|
5280
5434
|
for (;;) {
|
|
5281
5435
|
if (pages >= pageCap) throw new RebasePaginationError("max-pages", `Iterating "${label}" made ${pages} requests without the server reporting the end of the collection. Stopping rather than looping forever — raise \`maxPages\` if the walk is genuinely this long, or check that the backend sets \`meta.hasMore\`.`);
|
|
5282
5436
|
const pageParams = {
|
|
@@ -5353,9 +5507,16 @@ function createPaginationHelpers(find, label) {
|
|
|
5353
5507
|
* Relation values (`EntityRelation` instances or `{ __type: "relation", id, path }`
|
|
5354
5508
|
* objects) are serialized as their raw id — the wire format only carries the
|
|
5355
5509
|
* value to compare against the FK column.
|
|
5510
|
+
*
|
|
5511
|
+
* A `Date` is its ISO 8601 instant — what JSON makes of it on the socket and
|
|
5512
|
+
* in-process, so one filter names one instant through every door. `String()`
|
|
5513
|
+
* gave the viewer's local zone, no milliseconds and a parenthesised zone name
|
|
5514
|
+
* Postgres cannot parse. An invalid date keeps its "Invalid Date" spelling for
|
|
5515
|
+
* the server to refuse, rather than throwing a `RangeError` from here.
|
|
5356
5516
|
*/
|
|
5357
5517
|
function stringifyValue(value) {
|
|
5358
5518
|
if (value === null) return "null";
|
|
5519
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
5359
5520
|
const relation = normalizeToEntityRelation(value);
|
|
5360
5521
|
if (relation) return String(relation.id);
|
|
5361
5522
|
return String(value);
|
|
@@ -5413,6 +5574,41 @@ function unescapeWireValue(value) {
|
|
|
5413
5574
|
return result;
|
|
5414
5575
|
}
|
|
5415
5576
|
/**
|
|
5577
|
+
* Whether a scalar, sent raw as a query parameter's operand, would be read
|
|
5578
|
+
* back as something other than itself.
|
|
5579
|
+
*
|
|
5580
|
+
* A query parameter owns its whole value, so its scalar goes out unescaped —
|
|
5581
|
+
* `eq.a,b`, `eq.C:\x` — and the decoder returns it verbatim. Two shapes break
|
|
5582
|
+
* that. A parenthesised operand is a list for every operator (so that
|
|
5583
|
+
* `["==", ["a","b"]]` on an array column stays a list), so raw, the scalar
|
|
5584
|
+
* `"(deleted)"` would arrive as `["deleted"]`: an `==` matching nothing, a
|
|
5585
|
+
* `!=` returning the very rows it excludes. Such a scalar is sent escaped,
|
|
5586
|
+
* `eq.\(deleted\)`. And that spelling has to be told apart from a raw value
|
|
5587
|
+
* that looks the same, so a scalar of *that* shape is escaped in turn.
|
|
5588
|
+
*
|
|
5589
|
+
* Everything else stays raw, so a value an older client sends decodes as it
|
|
5590
|
+
* always has — unless it is exactly the escaped spelling of such a scalar,
|
|
5591
|
+
* which decodes to the scalar it spells.
|
|
5592
|
+
*/
|
|
5593
|
+
function scalarNeedsEscape(value) {
|
|
5594
|
+
if (value.startsWith("(") && value.endsWith(")")) return true;
|
|
5595
|
+
return decodeEscapedScalar(value) !== void 0;
|
|
5596
|
+
}
|
|
5597
|
+
/**
|
|
5598
|
+
* The scalar an operand is the escaped spelling of, when it is one.
|
|
5599
|
+
*
|
|
5600
|
+
* Exact on purpose: only an operand that {@link escapeWireValue} produces from
|
|
5601
|
+
* a scalar that {@link scalarNeedsEscape} is decoded. Any other operand —
|
|
5602
|
+
* including a raw backslash an older client sent, like `\\server\share` — is
|
|
5603
|
+
* left verbatim. Terminates because an operand that matches contains an
|
|
5604
|
+
* escape pair, so the value is strictly shorter than it.
|
|
5605
|
+
*/
|
|
5606
|
+
function decodeEscapedScalar(operand) {
|
|
5607
|
+
if (!operand.startsWith("\\")) return void 0;
|
|
5608
|
+
const value = unescapeWireValue(operand);
|
|
5609
|
+
return escapeWireValue(value) === operand && scalarNeedsEscape(value) ? value : void 0;
|
|
5610
|
+
}
|
|
5611
|
+
/**
|
|
5416
5612
|
* Split a parenthesized list string on unescaped commas.
|
|
5417
5613
|
* Input is the content between `(` and `)`.
|
|
5418
5614
|
*
|
|
@@ -5657,6 +5853,42 @@ function readTuple(field, raw) {
|
|
|
5657
5853
|
if (isOperatorShaped(op)) throw new UnknownFilterOperatorError(field, op);
|
|
5658
5854
|
}
|
|
5659
5855
|
/**
|
|
5856
|
+
* The query-parameter names a REST listing reads as something other than a
|
|
5857
|
+
* field filter: pagination, sorting, includes, projection, search, the logical
|
|
5858
|
+
* groups, the JSON `where` dialect, an aggregate's `select`/`groupBy`, and the
|
|
5859
|
+
* soft-delete switches.
|
|
5860
|
+
*
|
|
5861
|
+
* One list for both ends. The server skips these names when it collects
|
|
5862
|
+
* `?<field>=<op>.<value>` filters, and the SDK sends a filter on a column with
|
|
5863
|
+
* one of these names inside `?where=` instead — a collection is free to have a
|
|
5864
|
+
* `page`, `select` or `hard` column, and a filter on it sent as its own
|
|
5865
|
+
* parameter was read as the parameter and never applied.
|
|
5866
|
+
*/
|
|
5867
|
+
var RESERVED_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
5868
|
+
"limit",
|
|
5869
|
+
"offset",
|
|
5870
|
+
"page",
|
|
5871
|
+
"after",
|
|
5872
|
+
"orderBy",
|
|
5873
|
+
"include",
|
|
5874
|
+
"fields",
|
|
5875
|
+
"distinct",
|
|
5876
|
+
"searchString",
|
|
5877
|
+
"searchExplain",
|
|
5878
|
+
"vector_search",
|
|
5879
|
+
"vector",
|
|
5880
|
+
"vector_distance",
|
|
5881
|
+
"vector_threshold",
|
|
5882
|
+
"or",
|
|
5883
|
+
"and",
|
|
5884
|
+
"not",
|
|
5885
|
+
"where",
|
|
5886
|
+
"select",
|
|
5887
|
+
"groupBy",
|
|
5888
|
+
"deleted",
|
|
5889
|
+
"hard"
|
|
5890
|
+
]);
|
|
5891
|
+
/**
|
|
5660
5892
|
* Encode the `<op>.<value>` half of a wire condition.
|
|
5661
5893
|
*
|
|
5662
5894
|
* This is the single leaf encoder. Both wire positions that carry a condition
|
|
@@ -5684,7 +5916,7 @@ function serializeOperatorAndValue(op, value, { escapeScalar, where }) {
|
|
|
5684
5916
|
return `${restOp}.(${value.map((v) => escapeWireValue(stringifyValue(v))).join(",")})`;
|
|
5685
5917
|
}
|
|
5686
5918
|
const scalar = stringifyValue(value);
|
|
5687
|
-
return `${restOp}.${escapeScalar ? escapeWireValue(scalar) : scalar}`;
|
|
5919
|
+
return `${restOp}.${escapeScalar || !LIST_OPS.has(op) && scalarNeedsEscape(scalar) ? escapeWireValue(scalar) : scalar}`;
|
|
5688
5920
|
}
|
|
5689
5921
|
/**
|
|
5690
5922
|
* Serialize a single canonical condition tuple to a PostgREST dot-string.
|
|
@@ -5780,7 +6012,10 @@ var NULL_OPERANDS = /* @__PURE__ */ new Set([
|
|
|
5780
6012
|
* operand: only {@link NULL_OPERANDS}. `notnull.reason` is the value
|
|
5781
6013
|
* `"notnull.reason"`, not "reason is not null".
|
|
5782
6014
|
* - **Everything else** takes one scalar, and any remainder is one — including
|
|
5783
|
-
* the empty string, so `eq.` really is "equals the empty string".
|
|
6015
|
+
* the empty string, so `eq.` really is "equals the empty string". A
|
|
6016
|
+
* parenthesised remainder is a list for these too (`eq.(a,b)` is array
|
|
6017
|
+
* equality); a scalar that is itself parenthesised arrives escaped,
|
|
6018
|
+
* `eq.\(none\)`, and is decoded here — see {@link scalarNeedsEscape}.
|
|
5784
6019
|
*
|
|
5785
6020
|
* ### The one ambiguity that remains, and how to write past it
|
|
5786
6021
|
*
|
|
@@ -5815,7 +6050,7 @@ function deserializeSingle(raw) {
|
|
|
5815
6050
|
return [canonicalOp, inner === EMPTY_LIST_TOKEN ? [] : splitListItems(inner)];
|
|
5816
6051
|
}
|
|
5817
6052
|
if (LIST_OPS.has(canonicalOp)) return ["==", raw];
|
|
5818
|
-
return [canonicalOp, rest];
|
|
6053
|
+
return [canonicalOp, decodeEscapedScalar(rest) ?? rest];
|
|
5819
6054
|
}
|
|
5820
6055
|
/**
|
|
5821
6056
|
* Convert a PostgREST-style querystring record to `FilterValues`.
|
|
@@ -6995,6 +7230,6 @@ async function detectJunctionTables(executeSql) {
|
|
|
6995
7230
|
return junctionTables;
|
|
6996
7231
|
}
|
|
6997
7232
|
//#endregion
|
|
6998
|
-
export { ADMIN_ROLE, CALLBACK_REJECTED, COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, CursorError, CursorMismatchError, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, DEFAULT_STRING_COLUMN_LENGTH, IncludeSpecError, JUNCTION_TABLES_SQL, MAX_LOGICAL_NESTING_DEPTH, OrderBySpecError, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, REBASE_INTERNAL_TABLES, REBASE_USER_ROLE, RebasePaginationError, TENANT_INDEX_REASON, UnknownFilterOperatorError, aggregateAlias, and, applyDefaultValuesOnCreate, authSecretsMissingExclusion, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, buildTenantSecurityRule, callbackRefusal, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, canReadField, canWriteField, checkOperation, classifyTable, collectAllPages, cond, createDataSourceRegistry, createPaginationHelpers, createRelationRef, createRelationRefWithData, cursorToStartAfter, decodeCursor, defaultUsersCollection, defineCollection, denormalizeInclude, deserializeFilter, deserializeInclude, deserializeLogicalCondition, deserializeOrderBy, deserializeOrderByList, detectJunctionTables, effectiveAccess, embedParentExpression, encodeCursor, enumToObjectEntries, evaluateCondition, evaluatePolicy, fieldKeyForColumn, findAnonymousGrants, findRelation, firstSqlRow, fullPathToCollectionSegments, getArrayResolvedProperties, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionConfigForRelation, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, getTableVarName, getTenantConfig, hasFieldAccessRules, includePaths, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, isRelationRequired, isRelationalCollection, mergeIncludeSpecs, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeOrderBy, normalizeToEntityRelation, not, or, paginateFind, parseIdValues, parseOrderBySpecStrict, policyToPostgres, primaryOrderBy, reconcileCursorOrder, registerConditionOperations, relationDeclaringProperty, relationalCollections, requireCallbackClient, requireCallbackCollection, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveFindWindow, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, resolveTenantWrite, restrictedFieldNames, revokeInternalTableAccess, revokeInternalTableSql, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeInclude, serializeLogicalCondition, serializeOrderBy, sortCollectionsBySlug, sortProperties, sqlRows, sqlToPolicy, stripCollectionPath, tenantBypassRoles, tenantPolicyName, tenantScopeExpression, toCallbackError, toFilterTuples, topLevelIncludeNames, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, updateUserAutoValues, wrapAsEntityData, wrapAsSdkData };
|
|
7233
|
+
export { ADMIN_ROLE, CALLBACK_REJECTED, COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, CursorError, CursorMismatchError, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, DEFAULT_SOFT_DELETE_FIELD, DEFAULT_STRING_COLUMN_LENGTH, IncludeSpecError, JUNCTION_TABLES_SQL, MAX_LOGICAL_NESTING_DEPTH, OrderBySpecError, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, REBASE_INTERNAL_TABLES, REBASE_USER_ROLE, RESERVED_QUERY_KEYS, RebasePaginationError, TENANT_INDEX_REASON, UnknownFilterOperatorError, aggregateAlias, and, applyDefaultValuesOnCreate, assertBeforeQueryIsPostgresOnly, authSecretsMissingExclusion, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, buildTenantSecurityRule, callbackRefusal, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, canReadField, canWriteField, checkOperation, classifyTable, collectAllPages, cond, createDataSourceRegistry, createPaginationHelpers, createRelationRef, createRelationRefWithData, cursorToStartAfter, decodeCursor, defaultUsersCollection, defineCollection, denormalizeInclude, deserializeFilter, deserializeInclude, deserializeLogicalCondition, deserializeOrderBy, deserializeOrderByList, detectJunctionTables, effectiveAccess, embedParentExpression, encodeCursor, enumToObjectEntries, evaluateCondition, evaluatePolicy, fieldKeyForColumn, findAnonymousGrants, findRelation, firstSqlRow, fullPathToCollectionSegments, getArrayResolvedProperties, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionConfigForRelation, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, getTableVarName, getTenantConfig, hasFieldAccessRules, includePaths, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, isRelationRequired, isRelationalCollection, mergeIncludeSpecs, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeOrderBy, normalizeToEntityRelation, not, or, paginateFind, parseIdValues, parseOrderBySpecStrict, policyToPostgres, policyToSecurityRule, primaryOrderBy, reconcileCursorOrder, registerConditionOperations, relationDeclaringProperty, relationalCollections, requireCallbackClient, requireCallbackCollection, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveFindWindow, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, resolveTenantWrite, restoresSoftDeletedRow, restrictedFieldNames, revokeInternalTableAccess, revokeInternalTableSql, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeInclude, serializeLogicalCondition, serializeOrderBy, softDeleteFieldOf, sortCollectionsBySlug, sortProperties, sqlRows, sqlToPolicy, stripCollectionPath, tenantBypassRoles, tenantPolicyName, tenantScopeExpression, toCallbackError, toFilterTuples, topLevelIncludeNames, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, updateUserAutoValues, wrapAsEntityData, wrapAsSdkData };
|
|
6999
7234
|
|
|
7000
7235
|
//# sourceMappingURL=index.es.js.map
|