@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g27a562a
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/PostgresBackendDriver.d.ts +48 -1
- package/dist/{src-DlPBctw_.js → auth-users-columns-Do9mw5Y5.js} +318 -42
- package/dist/auth-users-columns-Do9mw5Y5.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
- package/dist/cli-helpers.d.ts +1 -1
- package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-DMUdRQdM.js} +2 -2
- package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-DMUdRQdM.js.map} +1 -1
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DSIxvLLD.js} +59 -15
- package/dist/ensure-collection-tables-DSIxvLLD.js.map +1 -0
- package/dist/index.es.js +534 -200
- package/dist/index.es.js.map +1 -1
- package/dist/{policy-CeA1JcxP.js → policy-CPkCqVTz.js} +4 -4
- package/dist/policy-CPkCqVTz.js.map +1 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/ensure-collection-tables.d.ts +1 -1
- package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/security/rls-enforcement.d.ts +53 -2
- package/dist/services/FetchService.d.ts +10 -7
- package/dist/services/dataService.d.ts +2 -0
- package/dist/services/realtimeService.d.ts +25 -21
- package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
- package/dist/src-C_wvdMnl.js.map +1 -0
- package/dist/{websocket-B2LsrINK.js → websocket-D0TBU3ia.js} +3 -3
- package/dist/{websocket-B2LsrINK.js.map → websocket-D0TBU3ia.js.map} +1 -1
- package/package.json +9 -8
- package/src/PostgresBackendDriver.ts +165 -3
- package/src/PostgresBootstrapper.ts +41 -2
- package/src/auth/ensure-tables.ts +185 -86
- package/src/cli-helpers.ts +22 -9
- package/src/cli.ts +175 -30
- package/src/collections/validate-relations.ts +124 -17
- package/src/data-transformer.ts +13 -3
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor.ts +7 -5
- package/src/schema/ensure-collection-tables.ts +88 -19
- package/src/schema/generate-drizzle-schema-logic.ts +8 -2
- package/src/schema/generate-postgres-ddl-logic.ts +97 -13
- package/src/schema/generated-schema-staleness.ts +169 -0
- package/src/schema/introspect-db-logic.ts +1 -1
- package/src/schema/non-sql-collections.test.ts +131 -0
- package/src/schema/rls-bootstrap-sql.ts +288 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/rls-enforcement.ts +141 -3
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +10 -93
- package/src/services/PersistService.ts +14 -1
- package/src/services/channel-history.ts +8 -0
- package/src/services/channel-presence.ts +6 -0
- package/src/services/dataService.ts +2 -0
- package/src/services/realtimeService.ts +36 -33
- package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
- package/dist/policy-CeA1JcxP.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-DlPBctw_.js.map +0 -1
- package/dist/src-DoU9yPqq.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -3,7 +3,7 @@ import { BranchService } from "./services/BranchService";
|
|
|
3
3
|
import { RealtimeService } from "./services/realtimeService";
|
|
4
4
|
import { DatabasePoolManager } from "./databasePoolManager";
|
|
5
5
|
import { DrizzleClient } from "./interfaces";
|
|
6
|
-
import { DatabaseAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseClient, RebaseSdkData, RestFetchService, SaveManyProps, SaveProps, TableMetadata, User } from "@rebasepro/types";
|
|
6
|
+
import { DatabaseAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseClient, RebaseSdkData, RestFetchService, SaveManyProps, SaveProps, UpdateManyProps, DeleteManyProps, TableMetadata, User } from "@rebasepro/types";
|
|
7
7
|
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
|
|
8
8
|
import { HistoryService } from "./history/HistoryService";
|
|
9
9
|
export declare class PostgresBackendDriver implements DataDriver {
|
|
@@ -62,6 +62,25 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
62
62
|
* REST/SDK read leaks unmasked data (see {@link applyAfterReadForRest}).
|
|
63
63
|
*/
|
|
64
64
|
get restFetchService(): RestFetchService;
|
|
65
|
+
/**
|
|
66
|
+
* Build the context handed to every collection callback.
|
|
67
|
+
*
|
|
68
|
+
* Note `data: this.data` — `this` is whichever driver is running the
|
|
69
|
+
* operation, so the callback's data plane inherits that driver's privilege.
|
|
70
|
+
* On a user request `AuthenticatedPostgresBackendDriver.withTransaction`
|
|
71
|
+
* constructs a fresh base driver bound to the RLS-scoped transaction and
|
|
72
|
+
* runs the operation on it, so `this.data` speaks through that connection
|
|
73
|
+
* and policies apply. On server-context work `this` is the base driver on
|
|
74
|
+
* the owner connection, and they do not. Pinned by the
|
|
75
|
+
* `"scopes context.data to the caller"` case in the `rls-enforcement` e2e
|
|
76
|
+
* suite, because it is the kind of property that is easy to break from a
|
|
77
|
+
* distance and impossible to notice.
|
|
78
|
+
*
|
|
79
|
+
* Previously returned through `as unknown as RebaseCallContext`, which
|
|
80
|
+
* disabled checking for the whole object and let `driver` — documented in
|
|
81
|
+
* the callbacks guide — sit on the runtime context while absent from the
|
|
82
|
+
* contract. Both are declared now, so this is a plain typed return.
|
|
83
|
+
*/
|
|
65
84
|
private buildCallContext;
|
|
66
85
|
private resolveCollectionCallbacks;
|
|
67
86
|
/**
|
|
@@ -102,6 +121,34 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
102
121
|
* with the last write winning, exactly as separate calls would.
|
|
103
122
|
*/
|
|
104
123
|
saveMany<M extends Record<string, unknown>>({ path, rows, collection, upsert }: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Update many rows through the same pipeline as {@link save}, in one
|
|
126
|
+
* transaction.
|
|
127
|
+
*
|
|
128
|
+
* Structurally the mirror of {@link saveMany} — same tx-bound sub-driver,
|
|
129
|
+
* same deferred notifications, same per-row error labelling — but it calls
|
|
130
|
+
* `save` with an explicit `id` and `status: "existing"`, which is precisely
|
|
131
|
+
* what `saveMany` cannot do: that one passes `status: "new"` and keeps the
|
|
132
|
+
* key inside `values`, so it inserts or upserts and can never target a
|
|
133
|
+
* particular row.
|
|
134
|
+
*
|
|
135
|
+
* All-or-nothing, so an id matching no row aborts the batch. A partial
|
|
136
|
+
* update is the outcome with no good recovery: the caller cannot tell which
|
|
137
|
+
* half landed without re-reading everything.
|
|
138
|
+
*/
|
|
139
|
+
updateMany<M extends Record<string, unknown>>({ path, updates, collection }: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;
|
|
140
|
+
/**
|
|
141
|
+
* Delete many rows in one transaction, running the full delete pipeline —
|
|
142
|
+
* `beforeDelete`, the delete, `afterDelete` — for each.
|
|
143
|
+
*
|
|
144
|
+
* Looping the single-row {@link delete} rather than emitting one
|
|
145
|
+
* `DELETE ... WHERE id = ANY($1)` is the deliberate choice: a single
|
|
146
|
+
* statement would be faster and would skip every callback, so a collection
|
|
147
|
+
* relying on `beforeDelete` to veto or on `afterDelete` to clean up
|
|
148
|
+
* dependents would behave differently depending on how many rows the caller
|
|
149
|
+
* happened to delete at once. Same pipeline, one transaction.
|
|
150
|
+
*/
|
|
151
|
+
deleteMany<M extends Record<string, unknown>>({ path, ids, collection }: DeleteManyProps<M>): Promise<void>;
|
|
105
152
|
delete<M extends Record<string, unknown>>({ row, collection }: DeleteProps<M>): Promise<void>;
|
|
106
153
|
deleteAll(path: string): Promise<void>;
|
|
107
154
|
checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean>;
|
|
@@ -2,8 +2,8 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { l as __require, s as __commonJSMin } from "./connection-BuZ97wsr.js";
|
|
5
|
-
import { a as
|
|
6
|
-
import { n as ANONYMOUS_USER_IDS, r as policy, t as ANONYMOUS_USER_ID } from "./policy-
|
|
5
|
+
import { a as rewriteLegacyRlsFunctions, c as isPostgresCollectionConfig, d as getDataSourceCapabilities, f as NULL_OPS, i as RLS_UID_SQL, l as isRelationalCollectionConfig, m as toCanonicalOp, p as REST_TO_CANONICAL, r as RLS_ROLES_SQL, s as getDeclaredSubcollections } from "./src-C_wvdMnl.js";
|
|
6
|
+
import { n as ANONYMOUS_USER_IDS, r as policy, t as ANONYMOUS_USER_ID } from "./policy-CPkCqVTz.js";
|
|
7
7
|
//#region ../types/src/types/entities.ts
|
|
8
8
|
/**
|
|
9
9
|
* Class used to create a reference to a entity in a different path
|
|
@@ -55,6 +55,25 @@ function hasForeignKeyOnTarget(relation) {
|
|
|
55
55
|
function isManyToMany(relation) {
|
|
56
56
|
return relation.kind === "manyToMany";
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
60
|
+
*
|
|
61
|
+
* - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
|
|
62
|
+
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
63
|
+
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
64
|
+
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
65
|
+
*
|
|
66
|
+
* The return is never `undefined` — no ingress that routes its client limit
|
|
67
|
+
* through this can produce an unbounded read.
|
|
68
|
+
*/
|
|
69
|
+
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
70
|
+
const maxLimit = opts.maxLimit ?? 1e3;
|
|
71
|
+
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
72
|
+
const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
|
|
73
|
+
if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
|
|
74
|
+
}
|
|
75
|
+
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
76
|
+
}
|
|
58
77
|
//#endregion
|
|
59
78
|
//#region ../common/src/util/common.ts
|
|
60
79
|
var DEFAULT_ONE_OF_TYPE = "type";
|
|
@@ -1007,6 +1026,35 @@ function camelCase(str) {
|
|
|
1007
1026
|
});
|
|
1008
1027
|
})))();
|
|
1009
1028
|
/**
|
|
1029
|
+
* Segments that reach the prototype chain rather than a property of the object.
|
|
1030
|
+
*
|
|
1031
|
+
* The twin of this function in `@rebasepro/forms` could be made to write onto
|
|
1032
|
+
* `Object.prototype` through a path of `__proto__.x`. This copy survives the
|
|
1033
|
+
* write by accident — its `clone` always spreads into a fresh object, while the
|
|
1034
|
+
* form engine's has a "preserve class instances" branch that hands back
|
|
1035
|
+
* `Object.prototype` itself — but `getIn` still *reads* through the chain, and
|
|
1036
|
+
* handing back `Object.prototype` is how a polluted value is read out again.
|
|
1037
|
+
*
|
|
1038
|
+
* Closed on both sides here, so the two implementations agree.
|
|
1039
|
+
*/
|
|
1040
|
+
var UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1041
|
+
"__proto__",
|
|
1042
|
+
"constructor",
|
|
1043
|
+
"prototype"
|
|
1044
|
+
]);
|
|
1045
|
+
/**
|
|
1046
|
+
* Whether writing this single key with `obj[key] = …` would reach the prototype
|
|
1047
|
+
* chain instead of creating a property.
|
|
1048
|
+
*
|
|
1049
|
+
* The single-key counterpart of {@link pathTraversesPrototype}, for the many
|
|
1050
|
+
* places that copy an object one key at a time. `JSON.parse` creates
|
|
1051
|
+
* `__proto__` as an *own* property, so it survives `hasOwnProperty` — and then
|
|
1052
|
+
* `target[key] = value` invokes the setter and replaces the target's prototype.
|
|
1053
|
+
*/
|
|
1054
|
+
function isPrototypePollutingKey(key) {
|
|
1055
|
+
return UNSAFE_PATH_SEGMENTS.has(key);
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1010
1058
|
* Deep clone a value, preserving function references and class instances.
|
|
1011
1059
|
* Unlike structuredClone, this handles objects that contain functions
|
|
1012
1060
|
* (e.g. CollectionConfig with target(), childCollections(), callbacks).
|
|
@@ -1383,10 +1431,21 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
|
|
|
1383
1431
|
* have `id` and `path` fields — these are relation-shaped objects from
|
|
1384
1432
|
* edge cases in the data pipeline (REST fallback, stale cache, custom data source).
|
|
1385
1433
|
*
|
|
1434
|
+
* When `targetPath` is given, also accepts a bare id. A relation column is a
|
|
1435
|
+
* foreign key, and the REST layer returns it as the scalar it is; only some
|
|
1436
|
+
* fetch paths hydrate it into an object. Which form a caller sees therefore
|
|
1437
|
+
* depends on how the row was loaded, and a caller that only accepted objects
|
|
1438
|
+
* reported half of its own data as a type error. The declared target is the
|
|
1439
|
+
* missing half: with it, an id is a relation that has not been fetched yet.
|
|
1440
|
+
*
|
|
1386
1441
|
* Returns null if the value cannot be coerced.
|
|
1387
1442
|
*/
|
|
1388
|
-
function normalizeToEntityRelation(value, propertyType) {
|
|
1443
|
+
function normalizeToEntityRelation(value, propertyType, targetPath) {
|
|
1389
1444
|
if (value instanceof EntityRelation) return value;
|
|
1445
|
+
if (targetPath && (typeof value === "string" || typeof value === "number")) {
|
|
1446
|
+
if (value === "") return null;
|
|
1447
|
+
return new EntityRelation(value, targetPath);
|
|
1448
|
+
}
|
|
1390
1449
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1391
1450
|
const obj = value;
|
|
1392
1451
|
if (!(obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference() || propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string")) return null;
|
|
@@ -1924,9 +1983,9 @@ function isKeywordAt(upper, i, keyword) {
|
|
|
1924
1983
|
*
|
|
1925
1984
|
* This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the
|
|
1926
1985
|
* `AND` inside
|
|
1927
|
-
* `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id =
|
|
1986
|
+
* `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = rebase.uid())`
|
|
1928
1987
|
* split the expression, and re-emitting the halves produced
|
|
1929
|
-
* `(EXISTS (...) AND m.user_id =
|
|
1988
|
+
* `(EXISTS (...) AND m.user_id = rebase.uid())`
|
|
1930
1989
|
* where `m` is no longer in scope — SQL that Postgres rejects outright with
|
|
1931
1990
|
* "missing FROM-clause entry for table". Returning null instead keeps such a
|
|
1932
1991
|
* clause as a `raw` expression, which round-trips verbatim.
|
|
@@ -2000,15 +2059,15 @@ function stripOuterParens(sql) {
|
|
|
2000
2059
|
}
|
|
2001
2060
|
}
|
|
2002
2061
|
function sqlToPolicy(sql) {
|
|
2003
|
-
const trimmed = stripOuterParens(sql.trim());
|
|
2062
|
+
const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());
|
|
2004
2063
|
if (trimmed.toLowerCase() === "true") return policy.true();
|
|
2005
2064
|
if (trimmed.toLowerCase() === "false") return policy.false();
|
|
2006
|
-
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*
|
|
2065
|
+
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
|
|
2007
2066
|
if (overlapMatch) {
|
|
2008
2067
|
const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
2009
2068
|
return policy.rolesOverlap(roles);
|
|
2010
2069
|
}
|
|
2011
|
-
const containMatch = trimmed.match(/^string_to_array\s*\(\s*
|
|
2070
|
+
const containMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
|
|
2012
2071
|
if (containMatch) {
|
|
2013
2072
|
const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
2014
2073
|
return policy.rolesContain(roles);
|
|
@@ -2024,10 +2083,10 @@ function sqlToPolicy(sql) {
|
|
|
2024
2083
|
const right = parseOperand(rightStr.trim());
|
|
2025
2084
|
if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
|
|
2026
2085
|
}
|
|
2027
|
-
return policy.raw(
|
|
2086
|
+
return policy.raw(trimmed);
|
|
2028
2087
|
}
|
|
2029
2088
|
/**
|
|
2030
|
-
* Literals from other BaaS platforms that people compare `
|
|
2089
|
+
* Literals from other BaaS platforms that people compare `rebase.uid()` against
|
|
2031
2090
|
* out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
|
|
2032
2091
|
* `pgRoles`, one surface over: the same muscle memory inside a `using:` string
|
|
2033
2092
|
* is the more dangerous spelling, because it inverts a rule instead of
|
|
@@ -2038,18 +2097,25 @@ var FOREIGN_CONVENTION_UIDS = {
|
|
|
2038
2097
|
authenticated: "Supabase",
|
|
2039
2098
|
service_role: "Supabase"
|
|
2040
2099
|
};
|
|
2041
|
-
/**
|
|
2042
|
-
|
|
2100
|
+
/**
|
|
2101
|
+
* `rebase.uid() IS NOT NULL` in raw SQL, the clause that is always true.
|
|
2102
|
+
*
|
|
2103
|
+
* Both schema spellings, because this runs over policy bodies read back from a
|
|
2104
|
+
* database, and one migrated by a pre-1.0 release still holds `auth.uid()`.
|
|
2105
|
+
* A security check that stops recognising a dangerous clause because the
|
|
2106
|
+
* framework renamed a function is a check that silently turns off.
|
|
2107
|
+
*/
|
|
2108
|
+
var UID_NOT_NULL = /\b(?:rebase|auth)\.uid\(\)\s+IS\s+NOT\s+NULL/i;
|
|
2043
2109
|
/**
|
|
2044
2110
|
* Find clauses that read as "signed-in users only" but admit anonymous callers.
|
|
2045
2111
|
*
|
|
2046
|
-
* Both spellings come from the same place — Supabase, where `auth.uid()`
|
|
2047
|
-
* is NULL for an anonymous request. Rebase substitutes
|
|
2112
|
+
* Both spellings come from the same place — Supabase, where its own `auth.uid()`
|
|
2113
|
+
* really is NULL for an anonymous request. Rebase substitutes
|
|
2048
2114
|
* {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which
|
|
2049
2115
|
* is how the trusted *server* context is recognised), so:
|
|
2050
2116
|
*
|
|
2051
|
-
* - `
|
|
2052
|
-
* - `
|
|
2117
|
+
* - `rebase.uid() IS NOT NULL` is a tautology on the user path, and
|
|
2118
|
+
* - `rebase.uid() != 'anon'` excludes one spelling of anonymous and admits the
|
|
2053
2119
|
* other. This one is not hypothetical and was not only a foreign habit:
|
|
2054
2120
|
* rebase's own request path reported `'anon'` while everything that compiled
|
|
2055
2121
|
* or checked a policy used `'anonymous'`, so whichever literal an author
|
|
@@ -2081,7 +2147,7 @@ function findAnonymousGrants(expr) {
|
|
|
2081
2147
|
if (UID_NOT_NULL.test(e.sql)) found.push({
|
|
2082
2148
|
pattern: "uid-not-null",
|
|
2083
2149
|
detail: e.sql,
|
|
2084
|
-
explanation: `\`
|
|
2150
|
+
explanation: `\`rebase.uid() IS NOT NULL\` is true for every request that came from a client, including anonymous ones — they carry '${ANONYMOUS_USER_ID}', not NULL. Use \`condition: policy.authenticated()\` to mean "signed in".`
|
|
2085
2151
|
});
|
|
2086
2152
|
return;
|
|
2087
2153
|
case "compare": {
|
|
@@ -2103,7 +2169,7 @@ function findAnonymousGrants(expr) {
|
|
|
2103
2169
|
return found;
|
|
2104
2170
|
}
|
|
2105
2171
|
function parseOperand(str) {
|
|
2106
|
-
if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /
|
|
2172
|
+
if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /rebase\.uid\(\)/i.test(str)) return policy.authUid();
|
|
2107
2173
|
const stringMatch = str.match(/^'(.+)'$/);
|
|
2108
2174
|
if (stringMatch) return policy.literal(stringMatch[1]);
|
|
2109
2175
|
if (/^\w+$/.test(str)) return policy.field(str);
|
|
@@ -2185,12 +2251,12 @@ function compile(expr, scope) {
|
|
|
2185
2251
|
const rightSql = castForAuthUid(expr.right, operandToSql(expr.right, scope), expr.left);
|
|
2186
2252
|
return `${leftSql} ${COMPARE_SQL[expr.op]} ${rightSql}`;
|
|
2187
2253
|
}
|
|
2188
|
-
case "rolesOverlap": return `string_to_array(
|
|
2189
|
-
case "rolesContain": return `string_to_array(
|
|
2190
|
-
case "authenticated": return
|
|
2191
|
-
case "serverContext": return
|
|
2254
|
+
case "rolesOverlap": return `string_to_array(${RLS_ROLES_SQL}, ',') && ${rolesArraySql(expr.roles)}`;
|
|
2255
|
+
case "rolesContain": return `string_to_array(${RLS_ROLES_SQL}, ',') @> ${rolesArraySql(expr.roles)}`;
|
|
2256
|
+
case "authenticated": return `${RLS_UID_SQL} IS NOT NULL AND ${RLS_UID_SQL} NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
|
|
2257
|
+
case "serverContext": return `${RLS_UID_SQL} IS NULL`;
|
|
2192
2258
|
case "existsIn": return compileExistsIn(expr, scope);
|
|
2193
|
-
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
2259
|
+
case "raw": return rewriteLegacyRlsFunctions(expr.sql).replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
2194
2260
|
}
|
|
2195
2261
|
}
|
|
2196
2262
|
/**
|
|
@@ -2227,8 +2293,8 @@ function operandToSql(operand, scope) {
|
|
|
2227
2293
|
case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
|
|
2228
2294
|
case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
|
|
2229
2295
|
case "literal": return quoteLiteral(operand.value);
|
|
2230
|
-
case "authUid": return
|
|
2231
|
-
case "authRoles": return
|
|
2296
|
+
case "authUid": return RLS_UID_SQL;
|
|
2297
|
+
case "authRoles": return `string_to_array(${RLS_ROLES_SQL}, ',')`;
|
|
2232
2298
|
}
|
|
2233
2299
|
}
|
|
2234
2300
|
/**
|
|
@@ -2308,7 +2374,7 @@ var DEFAULT_GUARDED_OPS = [
|
|
|
2308
2374
|
"delete"
|
|
2309
2375
|
];
|
|
2310
2376
|
/** Whether a collection is flagged as an authentication collection. */
|
|
2311
|
-
function isAuthCollection(collection) {
|
|
2377
|
+
function isAuthCollection$1(collection) {
|
|
2312
2378
|
const auth = collection.auth;
|
|
2313
2379
|
return auth === true || typeof auth === "object" && auth?.enabled === true;
|
|
2314
2380
|
}
|
|
@@ -2341,7 +2407,7 @@ function getEffectiveSecurityRules(collection) {
|
|
|
2341
2407
|
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
2342
2408
|
check: SERVER_OR_ADMIN_EXPR$1
|
|
2343
2409
|
});
|
|
2344
|
-
if (isAuthCollection(collection)) {
|
|
2410
|
+
if (isAuthCollection$1(collection)) {
|
|
2345
2411
|
injected.push({
|
|
2346
2412
|
name: `${tableName}_default_self_read`,
|
|
2347
2413
|
operations: ["select"],
|
|
@@ -3308,6 +3374,46 @@ function resolveDataSource(collection, registry) {
|
|
|
3308
3374
|
capabilities: getDataSourceCapabilities(engine)
|
|
3309
3375
|
};
|
|
3310
3376
|
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Does a SQL toolchain own this collection's storage?
|
|
3379
|
+
*
|
|
3380
|
+
* "Owns the storage" means: something generates a table for it, pushes that
|
|
3381
|
+
* table to a database, plans its RLS policies, and reports it as drifted when
|
|
3382
|
+
* the two disagree. That is true of a Postgres collection and false of a
|
|
3383
|
+
* Firestore or MongoDB one, whose documents live in a store Rebase never
|
|
3384
|
+
* migrates — and the two were never told apart. Every stage of the SQL
|
|
3385
|
+
* toolchain took "the collections" to mean *all* of them, so a Firestore
|
|
3386
|
+
* collection declared next to the Postgres ones got a `pgTable` in the
|
|
3387
|
+
* generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
|
|
3388
|
+
* `db push` include list — where its name shielding a same-named real table
|
|
3389
|
+
* from Atlas's exclude list is the one that can lose data.
|
|
3390
|
+
*
|
|
3391
|
+
* The answer is the resolved engine's {@link DataSourceCapabilities}, not a
|
|
3392
|
+
* name check: an engine registered through `registerDataSourceCapabilities`
|
|
3393
|
+
* gets the same treatment as the built-in ones.
|
|
3394
|
+
*
|
|
3395
|
+
* Deliberately answers **true** for an engine nobody has heard of. Build-time
|
|
3396
|
+
* tooling (the CLI, the schema generator) has no data-source registry to
|
|
3397
|
+
* resolve a `dataSource` key against, so an unknown key resolves to an unknown
|
|
3398
|
+
* engine — and the cost of the two mistakes is not symmetric. Wrongly
|
|
3399
|
+
* including a collection generates a table nothing writes to; wrongly excluding
|
|
3400
|
+
* one silently stops generating a table the app is serving from. Declare
|
|
3401
|
+
* `engine` on a collection that is not SQL-backed and this is exact.
|
|
3402
|
+
*/
|
|
3403
|
+
function isRelationalCollection(collection, registry) {
|
|
3404
|
+
return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
|
|
3405
|
+
}
|
|
3406
|
+
/**
|
|
3407
|
+
* The subset of `collections` a SQL toolchain owns — see
|
|
3408
|
+
* {@link isRelationalCollection}.
|
|
3409
|
+
*
|
|
3410
|
+
* Every stage that generates SQL from collections starts by calling this, so
|
|
3411
|
+
* the rule lives in one place rather than being re-decided per generator. It
|
|
3412
|
+
* keeps the input order.
|
|
3413
|
+
*/
|
|
3414
|
+
function relationalCollections(collections, registry) {
|
|
3415
|
+
return collections.filter((collection) => isRelationalCollection(collection, registry));
|
|
3416
|
+
}
|
|
3311
3417
|
//#endregion
|
|
3312
3418
|
//#region ../common/src/collections/CollectionRegistry.ts
|
|
3313
3419
|
var CollectionRegistry = class {
|
|
@@ -3660,6 +3766,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
|
|
|
3660
3766
|
Object.setPrototypeOf(this, RebasePaginationError.prototype);
|
|
3661
3767
|
}
|
|
3662
3768
|
};
|
|
3769
|
+
/**
|
|
3770
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
3771
|
+
*
|
|
3772
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
3773
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
3774
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
3775
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
3776
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
3777
|
+
* rows are the mildest of those outcomes.
|
|
3778
|
+
*
|
|
3779
|
+
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
3780
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
3781
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
3782
|
+
* look like it is paging by offset.
|
|
3783
|
+
*/
|
|
3784
|
+
function resolveFindWindow(params) {
|
|
3785
|
+
const limit = params?.limit ?? 50;
|
|
3786
|
+
const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
|
|
3787
|
+
return {
|
|
3788
|
+
limit,
|
|
3789
|
+
offset,
|
|
3790
|
+
driverOffset: params?.page != null ? offset : params?.offset
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3663
3793
|
function normalizePageSize(raw) {
|
|
3664
3794
|
if (raw === void 0 || !Number.isFinite(raw)) return 200;
|
|
3665
3795
|
return Math.max(1, Math.floor(raw));
|
|
@@ -3955,21 +4085,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3955
4085
|
const accessor = {
|
|
3956
4086
|
async find(params) {
|
|
3957
4087
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3958
|
-
const limit = params
|
|
3959
|
-
const offset = params?.offset ?? 0;
|
|
4088
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3960
4089
|
const fetchService = driver.restFetchService;
|
|
3961
4090
|
const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
|
|
3962
4091
|
filter,
|
|
3963
|
-
|
|
3964
|
-
|
|
4092
|
+
logical: params?.logical,
|
|
4093
|
+
limit,
|
|
4094
|
+
offset: driverOffset,
|
|
3965
4095
|
orderBy: params?.orderBy?.[0],
|
|
3966
4096
|
order: params?.orderBy?.[1],
|
|
3967
4097
|
searchString: params?.searchString
|
|
3968
4098
|
}, params?.include) : await driver.fetchCollection({
|
|
3969
4099
|
path: slug,
|
|
3970
|
-
limit
|
|
3971
|
-
offset:
|
|
4100
|
+
limit,
|
|
4101
|
+
offset: driverOffset,
|
|
3972
4102
|
filter,
|
|
4103
|
+
logical: params?.logical,
|
|
3973
4104
|
orderBy: params?.orderBy?.[0],
|
|
3974
4105
|
order: params?.orderBy?.[1],
|
|
3975
4106
|
searchString: params?.searchString
|
|
@@ -3979,7 +4110,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3979
4110
|
if (driver.count) {
|
|
3980
4111
|
total = await driver.count({
|
|
3981
4112
|
path: slug,
|
|
3982
|
-
filter
|
|
4113
|
+
filter,
|
|
4114
|
+
logical: params?.logical,
|
|
4115
|
+
searchString: params?.searchString
|
|
3983
4116
|
});
|
|
3984
4117
|
hasMore = offset + rows.length < total;
|
|
3985
4118
|
}
|
|
@@ -4031,22 +4164,39 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4031
4164
|
values: {}
|
|
4032
4165
|
} });
|
|
4033
4166
|
},
|
|
4167
|
+
updateMany: driver.updateMany ? async (updates) => {
|
|
4168
|
+
return (await driver.updateMany({
|
|
4169
|
+
path: slug,
|
|
4170
|
+
updates: updates.map((u) => ({
|
|
4171
|
+
id: u.id,
|
|
4172
|
+
values: u.data
|
|
4173
|
+
}))
|
|
4174
|
+
})).map((row) => rowToEntity(row, slug, getPks()));
|
|
4175
|
+
} : void 0,
|
|
4176
|
+
deleteMany: driver.deleteMany ? async (ids) => {
|
|
4177
|
+
await driver.deleteMany({
|
|
4178
|
+
path: slug,
|
|
4179
|
+
ids
|
|
4180
|
+
});
|
|
4181
|
+
} : void 0,
|
|
4034
4182
|
count: driver.count ? async (params) => {
|
|
4035
4183
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
4036
4184
|
return driver.count({
|
|
4037
4185
|
path: slug,
|
|
4038
|
-
filter
|
|
4186
|
+
filter,
|
|
4187
|
+
logical: params?.logical,
|
|
4188
|
+
searchString: params?.searchString
|
|
4039
4189
|
});
|
|
4040
4190
|
} : void 0,
|
|
4041
4191
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
4042
|
-
const limit = params
|
|
4043
|
-
const offset = params?.offset ?? 0;
|
|
4192
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
4044
4193
|
const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
|
|
4045
4194
|
return driver.listenCollection({
|
|
4046
4195
|
path: slug,
|
|
4047
|
-
limit
|
|
4048
|
-
offset:
|
|
4196
|
+
limit,
|
|
4197
|
+
offset: driverOffset,
|
|
4049
4198
|
filter: params?.where,
|
|
4199
|
+
logical: params?.logical,
|
|
4050
4200
|
orderBy: params?.orderBy?.[0],
|
|
4051
4201
|
order: params?.orderBy?.[1],
|
|
4052
4202
|
searchString: params?.searchString,
|
|
@@ -4054,7 +4204,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4054
4204
|
onUpdate({
|
|
4055
4205
|
data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
|
|
4056
4206
|
meta: {
|
|
4057
|
-
total: entities.length,
|
|
4207
|
+
total: offset + entities.length,
|
|
4058
4208
|
limit,
|
|
4059
4209
|
offset,
|
|
4060
4210
|
hasMore: entities.length >= limit
|
|
@@ -4231,9 +4381,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
4231
4381
|
async update(id, data) {
|
|
4232
4382
|
return entityToRow(await snap.update(id, data));
|
|
4233
4383
|
},
|
|
4384
|
+
async updateMany(updates) {
|
|
4385
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
4386
|
+
if (updates.length === 0) return [];
|
|
4387
|
+
if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
|
|
4388
|
+
return (await snap.updateMany(updates.map((u) => ({
|
|
4389
|
+
id: u.id,
|
|
4390
|
+
data: u.data
|
|
4391
|
+
})))).map(entityToRow);
|
|
4392
|
+
},
|
|
4234
4393
|
delete(id) {
|
|
4235
4394
|
return snap.delete(id);
|
|
4236
4395
|
},
|
|
4396
|
+
async deleteMany(ids) {
|
|
4397
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
4398
|
+
if (ids.length === 0) return;
|
|
4399
|
+
if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
|
|
4400
|
+
await snap.deleteMany(ids);
|
|
4401
|
+
},
|
|
4237
4402
|
count: snap.count ? (params) => snap.count(params) : void 0,
|
|
4238
4403
|
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
4239
4404
|
data: res.data.map(entityToRow),
|
|
@@ -4294,6 +4459,117 @@ function buildSdkData(driver) {
|
|
|
4294
4459
|
return wrapAsSdkData(buildRebaseData(driver));
|
|
4295
4460
|
}
|
|
4296
4461
|
//#endregion
|
|
4297
|
-
|
|
4462
|
+
//#region src/schema/auth-users-columns.ts
|
|
4463
|
+
/**
|
|
4464
|
+
* `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
|
|
4465
|
+
* — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
|
|
4466
|
+
* The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
|
|
4467
|
+
* separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
|
|
4468
|
+
* table that already holds a longer row.
|
|
4469
|
+
*/
|
|
4470
|
+
var AUTH_USERS_COLUMNS = [
|
|
4471
|
+
{
|
|
4472
|
+
column: "email",
|
|
4473
|
+
type: "TEXT",
|
|
4474
|
+
notNull: true
|
|
4475
|
+
},
|
|
4476
|
+
{
|
|
4477
|
+
column: "display_name",
|
|
4478
|
+
type: "TEXT"
|
|
4479
|
+
},
|
|
4480
|
+
{
|
|
4481
|
+
column: "photo_url",
|
|
4482
|
+
type: "TEXT"
|
|
4483
|
+
},
|
|
4484
|
+
{
|
|
4485
|
+
column: "roles",
|
|
4486
|
+
type: "TEXT[]",
|
|
4487
|
+
default: "'{}'",
|
|
4488
|
+
notNull: true
|
|
4489
|
+
},
|
|
4490
|
+
{
|
|
4491
|
+
column: "password_hash",
|
|
4492
|
+
type: "TEXT"
|
|
4493
|
+
},
|
|
4494
|
+
{
|
|
4495
|
+
column: "email_verified",
|
|
4496
|
+
type: "BOOLEAN",
|
|
4497
|
+
default: "FALSE",
|
|
4498
|
+
notNull: true
|
|
4499
|
+
},
|
|
4500
|
+
{
|
|
4501
|
+
column: "email_verification_token",
|
|
4502
|
+
type: "TEXT"
|
|
4503
|
+
},
|
|
4504
|
+
{
|
|
4505
|
+
column: "email_verification_sent_at",
|
|
4506
|
+
type: "TIMESTAMP WITH TIME ZONE"
|
|
4507
|
+
},
|
|
4508
|
+
{
|
|
4509
|
+
column: "is_anonymous",
|
|
4510
|
+
type: "BOOLEAN",
|
|
4511
|
+
default: "FALSE",
|
|
4512
|
+
notNull: true
|
|
4513
|
+
},
|
|
4514
|
+
{
|
|
4515
|
+
column: "metadata",
|
|
4516
|
+
type: "JSONB",
|
|
4517
|
+
default: "'{}'",
|
|
4518
|
+
notNull: true
|
|
4519
|
+
},
|
|
4520
|
+
{
|
|
4521
|
+
column: "tokens_valid_after",
|
|
4522
|
+
type: "TIMESTAMP WITH TIME ZONE"
|
|
4523
|
+
},
|
|
4524
|
+
{
|
|
4525
|
+
column: "created_at",
|
|
4526
|
+
type: "TIMESTAMP WITH TIME ZONE",
|
|
4527
|
+
default: "NOW()",
|
|
4528
|
+
notNull: true
|
|
4529
|
+
},
|
|
4530
|
+
{
|
|
4531
|
+
column: "updated_at",
|
|
4532
|
+
type: "TIMESTAMP WITH TIME ZONE",
|
|
4533
|
+
default: "NOW()",
|
|
4534
|
+
notNull: true
|
|
4535
|
+
}
|
|
4536
|
+
];
|
|
4537
|
+
var BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map((c) => [c.column, c]));
|
|
4538
|
+
/** Type + inline constraints, as they appear after the column name. */
|
|
4539
|
+
function authUsersColumnSql(spec) {
|
|
4540
|
+
return [
|
|
4541
|
+
spec.type,
|
|
4542
|
+
spec.default !== void 0 ? `DEFAULT ${spec.default}` : "",
|
|
4543
|
+
spec.notNull ? "NOT NULL" : ""
|
|
4544
|
+
].filter(Boolean).join(" ");
|
|
4545
|
+
}
|
|
4546
|
+
/**
|
|
4547
|
+
* The auth-owned definition for a physical column name, or `undefined` when
|
|
4548
|
+
* auth does not own it.
|
|
4549
|
+
*
|
|
4550
|
+
* Callers pass the RESOLVED column name (after `columnName` mapping), because
|
|
4551
|
+
* that is the only name the three creators agree on: the scaffold's users
|
|
4552
|
+
* collection spells the property `displayName` and the column `display_name`.
|
|
4553
|
+
*/
|
|
4554
|
+
function authUsersColumnDefinition(column) {
|
|
4555
|
+
const spec = BY_COLUMN.get(column);
|
|
4556
|
+
return spec ? authUsersColumnSql(spec) : void 0;
|
|
4557
|
+
}
|
|
4558
|
+
/**
|
|
4559
|
+
* Whether a collection is an auth collection, i.e. whether the definitions in
|
|
4560
|
+
* this module apply to its table at all.
|
|
4561
|
+
*
|
|
4562
|
+
* Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
|
|
4563
|
+
* that one takes a `CollectionConfig`, this one is called from DDL code paths
|
|
4564
|
+
* that hold looser objects, and both spellings must accept `auth: true` as well
|
|
4565
|
+
* as `auth: { enabled: true }`.
|
|
4566
|
+
*/
|
|
4567
|
+
function isAuthCollection(collection) {
|
|
4568
|
+
const auth = collection?.auth;
|
|
4569
|
+
if (auth === true) return true;
|
|
4570
|
+
return typeof auth === "object" && auth !== null && auth.enabled === true;
|
|
4571
|
+
}
|
|
4572
|
+
//#endregion
|
|
4573
|
+
export { generateForeignKeyName as A, hasForeignKeyOnTarget as B, getDeclaredPrimaryKeys as C, createRelationRefWithData as D, createRelationRef as E, camelCase as F, Vector as H, toSnakeCase as I, DEFAULT_ONE_OF_TYPE as L, getPolicyNamesForRule as M, isPrototypePollutingKey as N, normalizeToEntityRelation as O, mergeDeep as P, DEFAULT_ONE_OF_VALUE as R, buildCompositeId as S, parseIdValues as T, isManyToMany as V, getColumnName as _, buildSdkData as a, getTableVarName as b, resolveStringColumnLength as c, resolveJunctionSpecs as d, getEffectiveSecurityRules as f, findRelation as g, findAnonymousGrants as h, isAuthCollection as i, legacyForeignKeyName as j, updateDateAutoValues as k, getJunctionCollectionConfig as l, securityRuleToConditions as m, authUsersColumnDefinition as n, CollectionRegistry as o, policyToPostgres as p, authUsersColumnSql as r, relationalCollections as s, AUTH_USERS_COLUMNS as t, getJunctionSecurityRules as u, getEnumVarName as v, isAddressableId as w, resolveCollectionRelations as x, getTableName as y, resolveClientListLimit as z };
|
|
4298
4574
|
|
|
4299
|
-
//# sourceMappingURL=
|
|
4575
|
+
//# sourceMappingURL=auth-users-columns-Do9mw5Y5.js.map
|