@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g3660bd5
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/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-25nZOd87.js} +2 -2
- package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-25nZOd87.js.map} +1 -1
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DRkgVBV5.js} +2 -2
- package/dist/{ensure-collection-tables-CBQdOETu.js.map → ensure-collection-tables-DRkgVBV5.js.map} +1 -1
- package/dist/index.es.js +110 -93
- package/dist/index.es.js.map +1 -1
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- 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-DlPBctw_.js → src-Bs2ZzSi4.js} +92 -15
- package/dist/src-Bs2ZzSi4.js.map +1 -0
- package/package.json +6 -6
- package/src/cli.ts +79 -0
- package/src/collections/validate-relations.ts +124 -17
- package/src/data-transformer.ts +13 -3
- package/src/schema/generated-schema-staleness.ts +164 -0
- package/src/schema/introspect-db-logic.ts +1 -1
- package/src/services/FetchService.ts +10 -93
- package/src/services/PersistService.ts +14 -1
- package/src/services/dataService.ts +2 -0
- package/src/services/realtimeService.ts +36 -33
- package/dist/src-DlPBctw_.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { a as guardPoolAgainstDirtyRelease, i as createReadReplicaConnection, n as createDirectDatabaseConnection, o as pinSearchPath, r as createPostgresDatabaseConnection } from "./connection-BuZ97wsr.js";
|
|
5
|
-
import { A as
|
|
5
|
+
import { A as camelCase, C as normalizeToEntityRelation, D as getPolicyNamesForRule, E as legacyForeignKeyName, F as hasForeignKeyOnTarget, I as isManyToMany, L as Vector, O as isPrototypePollutingKey, P as resolveClientListLimit, S as createRelationRefWithData, T as generateForeignKeyName, _ as buildCompositeId, a as getJunctionSecurityRules, b as parseIdValues, c as policyToPostgres, d as findRelation, f as getColumnName, g as resolveCollectionRelations, h as getTableVarName, i as getJunctionCollectionConfig, j as toSnakeCase, k as mergeDeep, l as securityRuleToConditions, m as getTableName$1, n as CollectionRegistry, o as resolveJunctionSpecs, p as getEnumVarName, r as resolveStringColumnLength, s as getEffectiveSecurityRules, t as buildSdkData, u as findAnonymousGrants, v as getDeclaredPrimaryKeys, w as updateDateAutoValues, x as createRelationRef, y as isAddressableId } from "./src-Bs2ZzSi4.js";
|
|
6
6
|
import { n as isPostgresCollectionConfig, r as isRelationalCollectionConfig } from "./src-DoU9yPqq.js";
|
|
7
7
|
import { t as ANONYMOUS_USER_ID } from "./policy-CeA1JcxP.js";
|
|
8
8
|
import { t as createPostgresWebSocket } from "./websocket-B2LsrINK.js";
|
|
@@ -30,25 +30,6 @@ import { randomUUID } from "crypto";
|
|
|
30
30
|
function isChannelBusInstance(setting) {
|
|
31
31
|
return typeof setting?.publish === "function";
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
35
|
-
*
|
|
36
|
-
* - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
|
|
37
|
-
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
38
|
-
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
39
|
-
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
40
|
-
*
|
|
41
|
-
* The return is never `undefined` — no ingress that routes its client limit
|
|
42
|
-
* through this can produce an unbounded read.
|
|
43
|
-
*/
|
|
44
|
-
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
45
|
-
const maxLimit = opts.maxLimit ?? 1e3;
|
|
46
|
-
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
47
|
-
const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
|
|
48
|
-
if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
|
|
49
|
-
}
|
|
50
|
-
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
51
|
-
}
|
|
52
33
|
//#endregion
|
|
53
34
|
//#region ../common/src/util/email.ts
|
|
54
35
|
/**
|
|
@@ -1276,7 +1257,11 @@ function sanitizeAndConvertDates(obj) {
|
|
|
1276
1257
|
if (obj instanceof Date) return obj.toISOString();
|
|
1277
1258
|
if (typeof obj === "object") {
|
|
1278
1259
|
const newObj = {};
|
|
1279
|
-
for (const key in obj)
|
|
1260
|
+
for (const key in obj) {
|
|
1261
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
1262
|
+
if (isPrototypePollutingKey(key)) continue;
|
|
1263
|
+
newObj[key] = sanitizeAndConvertDates(obj[key]);
|
|
1264
|
+
}
|
|
1280
1265
|
return newObj;
|
|
1281
1266
|
}
|
|
1282
1267
|
if (typeof obj === "string") {
|
|
@@ -1305,6 +1290,7 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
1305
1290
|
if (relation.kind === "belongsTo") foreignKeys.add(relation.localKey);
|
|
1306
1291
|
});
|
|
1307
1292
|
for (const [key, value] of Object.entries(row)) {
|
|
1293
|
+
if (isPrototypePollutingKey(key)) continue;
|
|
1308
1294
|
const property = properties[key];
|
|
1309
1295
|
const effectiveValue = foreignKeys.has(key) && value === "" ? null : value;
|
|
1310
1296
|
if (!property) {
|
|
@@ -3796,51 +3782,6 @@ var FetchService = class {
|
|
|
3796
3782
|
return !!this.getQueryBuilder(tableName);
|
|
3797
3783
|
}
|
|
3798
3784
|
/**
|
|
3799
|
-
* Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
|
|
3800
|
-
* for efficient JOIN-based relation loading.
|
|
3801
|
-
* Returns null if the API is not available or the query fails.
|
|
3802
|
-
* Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
|
|
3803
|
-
*/
|
|
3804
|
-
async fetchWithDrizzleQuery(collectionPath, collection, options, include, idInfo, idInfoArray) {
|
|
3805
|
-
try {
|
|
3806
|
-
const table = getTableForCollection(collection, this.registry);
|
|
3807
|
-
const tableName = getTableName(table);
|
|
3808
|
-
const queryTarget = this.getQueryBuilder(tableName);
|
|
3809
|
-
if (!queryTarget?.findMany) return null;
|
|
3810
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
3811
|
-
const withConfig = {};
|
|
3812
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) if (include[0] === "*" || include.includes(key)) {
|
|
3813
|
-
const drizzleRelName = relation.relationName || key;
|
|
3814
|
-
withConfig[drizzleRelName] = true;
|
|
3815
|
-
}
|
|
3816
|
-
const queryOpts = { with: withConfig };
|
|
3817
|
-
if (options.limit) queryOpts.limit = options.limit;
|
|
3818
|
-
if (options.filter) {
|
|
3819
|
-
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
3820
|
-
if (filterConditions.length > 0) queryOpts.where = and(...filterConditions);
|
|
3821
|
-
}
|
|
3822
|
-
if (options.orderBy) {
|
|
3823
|
-
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
3824
|
-
if (orderByField) queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
|
|
3825
|
-
}
|
|
3826
|
-
return (await queryTarget.findMany(queryOpts)).map((row) => {
|
|
3827
|
-
const flat = {};
|
|
3828
|
-
for (const [k, v] of Object.entries(row)) if (Array.isArray(v)) flat[k] = v.map((item) => {
|
|
3829
|
-
const keys = Object.keys(item);
|
|
3830
|
-
const nestedObj = keys.find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
3831
|
-
if (nestedObj && keys.length <= 3) return { ...item[nestedObj] };
|
|
3832
|
-
return { ...item };
|
|
3833
|
-
});
|
|
3834
|
-
else if (typeof v === "object" && v !== null) flat[k] = { ...v };
|
|
3835
|
-
else flat[k] = v;
|
|
3836
|
-
return flat;
|
|
3837
|
-
});
|
|
3838
|
-
} catch (e) {
|
|
3839
|
-
logger.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back`, { error: e });
|
|
3840
|
-
return null;
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
|
-
/**
|
|
3844
3785
|
* Fallback path used when db.query is unavailable.
|
|
3845
3786
|
* The primary path uses db.query.findMany with `with` config, which
|
|
3846
3787
|
* loads all relations in a single query.
|
|
@@ -4091,7 +4032,8 @@ var PersistService = class {
|
|
|
4091
4032
|
if (error instanceof ApiError || error?.name === "ApiError") return error;
|
|
4092
4033
|
const pgError = extractPgError(error);
|
|
4093
4034
|
if (pgError) {
|
|
4094
|
-
const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
|
|
4035
|
+
const { message, code } = pgErrorToFriendlyMessage(pgError, collectionSlug);
|
|
4036
|
+
if (/^2[23]/.test(code)) return code === "23505" ? ApiError.conflict(message, `PG_${code}`) : ApiError.badRequest(message, `PG_${code}`);
|
|
4095
4037
|
return new Error(message);
|
|
4096
4038
|
}
|
|
4097
4039
|
const causeMessage = extractCauseMessage(error);
|
|
@@ -7816,23 +7758,18 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
7816
7758
|
path: request.path,
|
|
7817
7759
|
collectionRequest: {
|
|
7818
7760
|
filter: request.filter,
|
|
7761
|
+
logical: request.logical,
|
|
7819
7762
|
orderBy: request.orderBy,
|
|
7820
7763
|
order: request.order,
|
|
7821
7764
|
limit: boundedLimit,
|
|
7765
|
+
offset: request.offset,
|
|
7822
7766
|
startAfter: request.startAfter,
|
|
7823
7767
|
databaseId: request.collection?.databaseId,
|
|
7824
7768
|
searchString: request.searchString
|
|
7825
7769
|
},
|
|
7826
7770
|
authContext
|
|
7827
7771
|
});
|
|
7828
|
-
const rows = await this.fetchCollectionWithAuth(request.path,
|
|
7829
|
-
filter: request.filter,
|
|
7830
|
-
orderBy: request.orderBy,
|
|
7831
|
-
order: request.order,
|
|
7832
|
-
limit: boundedLimit,
|
|
7833
|
-
startAfter: request.startAfter,
|
|
7834
|
-
searchString: request.searchString
|
|
7835
|
-
}, authContext);
|
|
7772
|
+
const rows = await this.fetchCollectionWithAuth(request.path, this._subscriptions.get(subscriptionId).collectionRequest, authContext);
|
|
7836
7773
|
this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
|
|
7837
7774
|
} catch (error) {
|
|
7838
7775
|
const sanitized = sanitizeErrorForClient(error, request.path);
|
|
@@ -8016,6 +7953,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
8016
7953
|
});
|
|
8017
7954
|
else fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
|
|
8018
7955
|
filter: collectionRequest.filter,
|
|
7956
|
+
logical: collectionRequest.logical,
|
|
8019
7957
|
orderBy: collectionRequest.orderBy,
|
|
8020
7958
|
order: collectionRequest.order,
|
|
8021
7959
|
limit: collectionRequest.limit,
|
|
@@ -9203,6 +9141,54 @@ var quote = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
|
|
|
9203
9141
|
/** `on.from` / `on.to` accept a single column or a composite tuple. */
|
|
9204
9142
|
var asColumns = (value) => Array.isArray(value) ? value : [value];
|
|
9205
9143
|
/**
|
|
9144
|
+
* Distinguish "this column name is wrong" from "the generated schema is old".
|
|
9145
|
+
*
|
|
9146
|
+
* They present identically here — a relation asks for a column the registered
|
|
9147
|
+
* table does not have — but they are opposite problems with opposite fixes, and
|
|
9148
|
+
* getting them the wrong way round is how the 0.12 → 0.13 upgrade bricked
|
|
9149
|
+
* projects.
|
|
9150
|
+
*
|
|
9151
|
+
* The registered table is not the database. It comes from the project's
|
|
9152
|
+
* checked-in `backend/src/schema.generated.ts`, and 0.13 changed the rule that
|
|
9153
|
+
* derives foreign-key names: `categories` yields `category_id` where it used to
|
|
9154
|
+
* yield `categorie_id`. Boot-ensure renames the database column to match, so by
|
|
9155
|
+
* the time this runs the *database* is correct and the *generated module* is the
|
|
9156
|
+
* stale one. Reporting "not a column" then points at the wrong artifact, and the
|
|
9157
|
+
* generic fix — "set `through.targetColumn` to one of: …", listing the legacy
|
|
9158
|
+
* name because that is what the stale module still has — talks the reader into
|
|
9159
|
+
* pinning a column that no longer exists.
|
|
9160
|
+
*
|
|
9161
|
+
* So when the wanted name is what the current rule derives, and the table
|
|
9162
|
+
* carries what the *previous* rule would have derived from the same source, say
|
|
9163
|
+
* that instead.
|
|
9164
|
+
*
|
|
9165
|
+
* @param wanted the column the relation asks for
|
|
9166
|
+
* @param available every column the registered table has
|
|
9167
|
+
* @param sources names the default could have been derived from (a slug, a
|
|
9168
|
+
* relation name) — checking against these rather than guessing
|
|
9169
|
+
* backwards from `wanted` keeps the match exact
|
|
9170
|
+
*/
|
|
9171
|
+
function staleCodegenRename(wanted, available, sources) {
|
|
9172
|
+
for (const source of sources) {
|
|
9173
|
+
if (!source) continue;
|
|
9174
|
+
const current = generateForeignKeyName(source);
|
|
9175
|
+
const legacy = legacyForeignKeyName(source);
|
|
9176
|
+
if (current !== wanted || legacy === current) continue;
|
|
9177
|
+
if (available.has(legacy) && !available.has(current)) return {
|
|
9178
|
+
legacy,
|
|
9179
|
+
current
|
|
9180
|
+
};
|
|
9181
|
+
}
|
|
9182
|
+
return null;
|
|
9183
|
+
}
|
|
9184
|
+
/** The shared explanation, so every relation kind reports it identically. */
|
|
9185
|
+
function staleCodegenDefect(table, { legacy, current }) {
|
|
9186
|
+
return {
|
|
9187
|
+
problem: `the generated Drizzle schema still declares \`${legacy}\` on \`${table}\`, but this release derives \`${current}\` — the generated schema predates the foreign-key naming fix and no longer describes the database`,
|
|
9188
|
+
fix: `regenerate it with \`rebase schema generate\` (or \`pnpm run schema:generate\`). The database column has already been renamed for you at boot, so nothing else is needed. To keep \`${legacy}\` instead, name it explicitly on the relation and regenerate.`
|
|
9189
|
+
};
|
|
9190
|
+
}
|
|
9191
|
+
/**
|
|
9206
9192
|
* Relations whose names do not resolve against the registered schema.
|
|
9207
9193
|
*
|
|
9208
9194
|
* Fails open wherever it cannot see enough to be sure — an unregistered source
|
|
@@ -9249,19 +9235,31 @@ function findRelationDefects(collections, registry) {
|
|
|
9249
9235
|
const targetColumns = columnNames(targetTable);
|
|
9250
9236
|
switch (relation.kind) {
|
|
9251
9237
|
case "belongsTo":
|
|
9252
|
-
if (!sourceColumns.has(relation.localKey))
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9238
|
+
if (!sourceColumns.has(relation.localKey)) {
|
|
9239
|
+
const stale = staleCodegenRename(relation.localKey, sourceColumns, [relation.relationName, targetCollection.slug]);
|
|
9240
|
+
defects.push(stale ? {
|
|
9241
|
+
...at,
|
|
9242
|
+
...staleCodegenDefect(sourceTableName, stale)
|
|
9243
|
+
} : {
|
|
9244
|
+
...at,
|
|
9245
|
+
problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
|
|
9246
|
+
fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
|
|
9247
|
+
});
|
|
9248
|
+
}
|
|
9257
9249
|
break;
|
|
9258
9250
|
case "hasOne":
|
|
9259
9251
|
case "hasMany":
|
|
9260
|
-
if (!targetColumns.has(relation.foreignKeyOnTarget))
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9252
|
+
if (!targetColumns.has(relation.foreignKeyOnTarget)) {
|
|
9253
|
+
const stale = staleCodegenRename(relation.foreignKeyOnTarget, targetColumns, [collection.slug]);
|
|
9254
|
+
defects.push(stale ? {
|
|
9255
|
+
...at,
|
|
9256
|
+
...staleCodegenDefect(targetTableName, stale)
|
|
9257
|
+
} : {
|
|
9258
|
+
...at,
|
|
9259
|
+
problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
|
|
9260
|
+
fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
|
|
9261
|
+
});
|
|
9262
|
+
}
|
|
9265
9263
|
if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) defects.push({
|
|
9266
9264
|
...at,
|
|
9267
9265
|
problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
|
|
@@ -9280,11 +9278,21 @@ function findRelationDefects(collections, registry) {
|
|
|
9280
9278
|
break;
|
|
9281
9279
|
}
|
|
9282
9280
|
const junctionColumns = columnNames(junction);
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9281
|
+
const derivedFrom = {
|
|
9282
|
+
sourceColumn: [collection.slug],
|
|
9283
|
+
targetColumn: [targetCollection.slug]
|
|
9284
|
+
};
|
|
9285
|
+
for (const [label, column] of [["sourceColumn", sourceColumn], ["targetColumn", targetColumn]]) if (!junctionColumns.has(column)) {
|
|
9286
|
+
const stale = staleCodegenRename(column, junctionColumns, [...derivedFrom[label]]);
|
|
9287
|
+
defects.push(stale ? {
|
|
9288
|
+
...at,
|
|
9289
|
+
...staleCodegenDefect(table, stale)
|
|
9290
|
+
} : {
|
|
9291
|
+
...at,
|
|
9292
|
+
problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
|
|
9293
|
+
fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
|
|
9294
|
+
});
|
|
9295
|
+
}
|
|
9288
9296
|
break;
|
|
9289
9297
|
}
|
|
9290
9298
|
case "via": {
|
|
@@ -9356,7 +9364,16 @@ function assertRelationsResolve(collections, registry) {
|
|
|
9356
9364
|
const defects = findRelationDefects(collections, registry);
|
|
9357
9365
|
if (defects.length === 0) return;
|
|
9358
9366
|
const lines = defects.map((d) => ` • ${d.collection}.${d.relationName} (${d.kind})\n ${d.problem}\n fix: ${d.fix}`);
|
|
9359
|
-
throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against
|
|
9367
|
+
throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against \`backend/src/schema.generated.ts\`.
|
|
9368
|
+
|
|
9369
|
+
Each of these would return no rows at query time rather than reporting an error, so they are fatal at boot instead.
|
|
9370
|
+
|
|
9371
|
+
If the database was migrated recently — an upgrade, a \`db push\`, a restore — this file is
|
|
9372
|
+
probably older than the schema it describes. Regenerate it before changing anything else:
|
|
9373
|
+
|
|
9374
|
+
rebase schema generate
|
|
9375
|
+
|
|
9376
|
+
If it is already current, then the collection is what disagrees with it:
|
|
9360
9377
|
|
|
9361
9378
|
` + lines.join("\n\n") + "\n");
|
|
9362
9379
|
}
|
|
@@ -12288,7 +12305,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12288
12305
|
*/
|
|
12289
12306
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
12290
12307
|
const internals = driverResult.internals;
|
|
12291
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
12308
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-DRkgVBV5.js");
|
|
12292
12309
|
const plan = await ensureCollectionTables({ async query(text) {
|
|
12293
12310
|
const result = await internals.db.execute(sql.raw(text));
|
|
12294
12311
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|
|
@@ -12313,7 +12330,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12313
12330
|
*/
|
|
12314
12331
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
12315
12332
|
const internals = driverResult.internals;
|
|
12316
|
-
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-
|
|
12333
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-25nZOd87.js");
|
|
12317
12334
|
const outcome = await ensureCollectionPolicies({ async query(text) {
|
|
12318
12335
|
const result = await internals.db.execute(sql.raw(text));
|
|
12319
12336
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|