@warlock.js/cascade 4.6.0 → 4.6.1
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/CHANGELOG.md +7 -0
- package/cjs/index.cjs +95 -30
- package/cjs/index.cjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +5 -4
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts +53 -13
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +90 -26
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/llms-full.txt +11 -7
- package/llms.txt +1 -1
- package/package.json +4 -4
- package/skills/manage-transactions/SKILL.md +11 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to `@warlock.js/cascade` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 4.6.1
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Native Postgres array columns (`TEXT[]` / `JSONB[]`, from `arrayText()` / `arrayJson()`) are now auto-detected by introspecting the schema on connect and bound as raw arrays — no more "malformed array literal" on insert and no need to hand-list `nativeArrayColumns` (which stays as an optional per-connection override, now consulted per-table)
|
|
12
|
+
- `transaction()` now flat-nests: a nested `transaction()` joins the active one (same session, sees its uncommitted writes) instead of opening a second, independent transaction — fixes phantom foreign-key violations when a service that opens its own transaction is called inside an outer one (e.g. a seeder creating a row, then a service inserting a child that references it). MongoDB joins too, replacing its "nested not supported" throw
|
|
13
|
+
|
|
7
14
|
## 4.6.0
|
|
8
15
|
|
|
9
16
|
### Added
|
package/cjs/index.cjs
CHANGED
|
@@ -13035,7 +13035,10 @@ var MongoDbDriver = class {
|
|
|
13035
13035
|
* @throws {Error} If transaction fails, is explicitly rolled back, or replica set not configured
|
|
13036
13036
|
*/
|
|
13037
13037
|
async transaction(fn, options) {
|
|
13038
|
-
|
|
13038
|
+
const ctx = { rollback(reason) {
|
|
13039
|
+
throw new TransactionRollbackError(reason);
|
|
13040
|
+
} };
|
|
13041
|
+
if (databaseTransactionContext.hasActiveTransaction()) return fn(ctx);
|
|
13039
13042
|
await this.ensureReplicaSetAvailable();
|
|
13040
13043
|
const session = this.getClientInstance().startSession();
|
|
13041
13044
|
try {
|
|
@@ -13045,9 +13048,7 @@ var MongoDbDriver = class {
|
|
|
13045
13048
|
});
|
|
13046
13049
|
databaseTransactionContext.enter({ session });
|
|
13047
13050
|
try {
|
|
13048
|
-
const result = await fn(
|
|
13049
|
-
throw new TransactionRollbackError(reason);
|
|
13050
|
-
} });
|
|
13051
|
+
const result = await fn(ctx);
|
|
13051
13052
|
await session.commitTransaction();
|
|
13052
13053
|
return result;
|
|
13053
13054
|
} catch (error) {
|
|
@@ -16925,13 +16926,22 @@ var PostgresDriver = class {
|
|
|
16925
16926
|
*/
|
|
16926
16927
|
_syncAdapter;
|
|
16927
16928
|
/**
|
|
16928
|
-
*
|
|
16929
|
-
* (`JSONB[]`, `TEXT[]`, …) and must NOT be JSON-text
|
|
16929
|
+
* Explicit, table-agnostic override list of column names that hold native
|
|
16930
|
+
* PostgreSQL arrays (`JSONB[]`, `TEXT[]`, …) and must NOT be JSON-text
|
|
16931
|
+
* encoded. Merged with (and superseded per-table by) the schema
|
|
16932
|
+
* introspection below; kept as a manual escape hatch.
|
|
16930
16933
|
*
|
|
16931
16934
|
* @see PostgresPoolConfig.nativeArrayColumns
|
|
16932
16935
|
*/
|
|
16933
16936
|
_nativeArrayColumns;
|
|
16934
16937
|
/**
|
|
16938
|
+
* Native-array columns discovered by introspecting the live schema on
|
|
16939
|
+
* connect, keyed `table → { column, … }`. Authoritative and table-scoped, so
|
|
16940
|
+
* a column that is `TEXT[]` in one table and `jsonb` in another is encoded
|
|
16941
|
+
* correctly for each — no app configuration required.
|
|
16942
|
+
*/
|
|
16943
|
+
_introspectedArrayColumns = /* @__PURE__ */ new Map();
|
|
16944
|
+
/**
|
|
16935
16945
|
* Create a new PostgreSQL driver instance.
|
|
16936
16946
|
*
|
|
16937
16947
|
* @param config - PostgreSQL connection configuration
|
|
@@ -16997,6 +17007,7 @@ var PostgresDriver = class {
|
|
|
16997
17007
|
(await this._pool.connect()).release();
|
|
16998
17008
|
_warlock_js_logger.log.success("database.postgres", "connection", `Connected to database ${_mongez_copper.colors.bold(_mongez_copper.colors.yellowBright(this.config.database))}`);
|
|
16999
17009
|
this._isConnected = true;
|
|
17010
|
+
await this.loadNativeArrayColumns();
|
|
17000
17011
|
this.emit("connected");
|
|
17001
17012
|
} catch (error) {
|
|
17002
17013
|
_warlock_js_logger.log.fatal("database.postgres", "connection", "Failed to connect to database");
|
|
@@ -17033,13 +17044,15 @@ var PostgresDriver = class {
|
|
|
17033
17044
|
* that need special handling for PostgreSQL storage.
|
|
17034
17045
|
*
|
|
17035
17046
|
* @param data - The data object to serialize
|
|
17047
|
+
* @param table - Optional table name; when given, columns introspected as
|
|
17048
|
+
* native arrays on that table are bound raw (see {@link serializeValue}).
|
|
17036
17049
|
* @returns Serialized data ready for PostgreSQL
|
|
17037
17050
|
*/
|
|
17038
|
-
serialize(data) {
|
|
17051
|
+
serialize(data, table) {
|
|
17039
17052
|
const serialized = {};
|
|
17040
17053
|
for (const [key, value] of Object.entries(data)) {
|
|
17041
17054
|
if (value === void 0) continue;
|
|
17042
|
-
serialized[key] = this.serializeValue(key, value);
|
|
17055
|
+
serialized[key] = this.serializeValue(key, value, table);
|
|
17043
17056
|
}
|
|
17044
17057
|
return serialized;
|
|
17045
17058
|
}
|
|
@@ -17059,36 +17072,87 @@ var PostgresDriver = class {
|
|
|
17059
17072
|
* `JSON.stringify`. node-pg renders a raw JS array as a PostgreSQL array
|
|
17060
17073
|
* literal `{...}` (and `[]` as `{}`), which a `json` / `jsonb` column
|
|
17061
17074
|
* rejects — so we bind the value as JSON text instead, the form those
|
|
17062
|
-
* columns accept. Columns
|
|
17063
|
-
*
|
|
17064
|
-
*
|
|
17075
|
+
* columns accept. Columns known to be native arrays — via schema
|
|
17076
|
+
* introspection or the `nativeArrayColumns` config — are exempt: their raw
|
|
17077
|
+
* array is passed through so node-pg emits the `{...}` literal a genuine
|
|
17078
|
+
* `JSONB[]` / `TEXT[]` column needs.
|
|
17065
17079
|
* - plain object → `JSON.stringify`. Equivalent to node-pg's own object
|
|
17066
17080
|
* handling, made explicit so both write paths agree.
|
|
17067
17081
|
* - everything else (scalars: string, number, boolean, null) → untouched.
|
|
17068
17082
|
*
|
|
17069
|
-
*
|
|
17070
|
-
*
|
|
17071
|
-
*
|
|
17072
|
-
*
|
|
17073
|
-
*
|
|
17074
|
-
*
|
|
17083
|
+
* Distinguishing native-array from `json` / `jsonb` columns: a value alone
|
|
17084
|
+
* can't tell them apart, so the driver introspects the live schema on connect
|
|
17085
|
+
* (see {@link loadNativeArrayColumns}) and consults that per-table map here
|
|
17086
|
+
* via {@link isNativeArrayColumn}. The explicit `nativeArrayColumns` config
|
|
17087
|
+
* still works as a table-agnostic override. No `::jsonb` placeholder cast is
|
|
17088
|
+
* added: a JSON-text string binds correctly to `json` / `jsonb` without one,
|
|
17089
|
+
* and a blind cast would misfire on columns we cannot positively identify as
|
|
17090
|
+
* jsonb.
|
|
17075
17091
|
*
|
|
17076
|
-
* @param key - Column name (used to
|
|
17092
|
+
* @param key - Column name (used to resolve native-array columns)
|
|
17077
17093
|
* @param value - The raw value to serialize (never `undefined`)
|
|
17094
|
+
* @param table - Optional table name; enables the per-table native-array lookup
|
|
17078
17095
|
* @returns The value ready to bind as a query parameter
|
|
17079
17096
|
*/
|
|
17080
|
-
serializeValue(key, value) {
|
|
17097
|
+
serializeValue(key, value, table) {
|
|
17081
17098
|
if (value instanceof Date) return value.toISOString();
|
|
17082
17099
|
if (typeof value === "bigint") return value.toString();
|
|
17083
17100
|
if (Array.isArray(value)) {
|
|
17084
17101
|
if (value.length > 0 && value.every((v) => typeof v === "number")) return `[${value.join(",")}]`;
|
|
17085
|
-
if (this.
|
|
17102
|
+
if (this.isNativeArrayColumn(table, key)) return value;
|
|
17086
17103
|
return JSON.stringify(value);
|
|
17087
17104
|
}
|
|
17088
17105
|
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
17089
17106
|
return value;
|
|
17090
17107
|
}
|
|
17091
17108
|
/**
|
|
17109
|
+
* Whether `column` on `table` is a native PostgreSQL array. True when the
|
|
17110
|
+
* connect-time schema introspection saw it as `data_type = 'ARRAY'` for that
|
|
17111
|
+
* table (authoritative, per-table), or when it's listed in the table-agnostic
|
|
17112
|
+
* `nativeArrayColumns` config override.
|
|
17113
|
+
*/
|
|
17114
|
+
isNativeArrayColumn(table, column) {
|
|
17115
|
+
if (table && this._introspectedArrayColumns.get(table)?.has(column)) return true;
|
|
17116
|
+
return this._nativeArrayColumns.has(column);
|
|
17117
|
+
}
|
|
17118
|
+
/**
|
|
17119
|
+
* Introspect the live schema for native-array columns so array values bind
|
|
17120
|
+
* correctly with zero app configuration.
|
|
17121
|
+
*
|
|
17122
|
+
* A JS array must be bound two opposite ways depending on the column: as JSON
|
|
17123
|
+
* text for a `json` / `jsonb` column, but as a raw array (which node-pg
|
|
17124
|
+
* renders `{...}`) for a native `TEXT[]` / `JSONB[]` / `INTEGER[]` column. The
|
|
17125
|
+
* serializer sees values, not types, so without this it JSON-stringifies
|
|
17126
|
+
* every array — which a native-array column rejects with "malformed array
|
|
17127
|
+
* literal". One `information_schema` query at connect, cached for the
|
|
17128
|
+
* connection lifetime, removes the need to hand-list `nativeArrayColumns`.
|
|
17129
|
+
*
|
|
17130
|
+
* Best-effort: any failure (e.g. restricted catalog access) is logged and
|
|
17131
|
+
* leaves the map empty so the config override still applies — it never blocks
|
|
17132
|
+
* connect. A schema change made within a live connection isn't reflected
|
|
17133
|
+
* until the next connect.
|
|
17134
|
+
*/
|
|
17135
|
+
async loadNativeArrayColumns() {
|
|
17136
|
+
try {
|
|
17137
|
+
const result = await this.query(`SELECT table_name, column_name
|
|
17138
|
+
FROM information_schema.columns
|
|
17139
|
+
WHERE table_schema = ANY (current_schemas(false))
|
|
17140
|
+
AND data_type = 'ARRAY'`);
|
|
17141
|
+
const map = /* @__PURE__ */ new Map();
|
|
17142
|
+
for (const { table_name, column_name } of result.rows) {
|
|
17143
|
+
let columns = map.get(table_name);
|
|
17144
|
+
if (!columns) {
|
|
17145
|
+
columns = /* @__PURE__ */ new Set();
|
|
17146
|
+
map.set(table_name, columns);
|
|
17147
|
+
}
|
|
17148
|
+
columns.add(column_name);
|
|
17149
|
+
}
|
|
17150
|
+
this._introspectedArrayColumns = map;
|
|
17151
|
+
} catch {
|
|
17152
|
+
_warlock_js_logger.log.warn("database.postgres", "introspection", "Could not introspect native-array columns; using the nativeArrayColumns config only");
|
|
17153
|
+
}
|
|
17154
|
+
}
|
|
17155
|
+
/**
|
|
17092
17156
|
* Get the dirty tracker for this driver.
|
|
17093
17157
|
*/
|
|
17094
17158
|
getDirtyTracker(data) {
|
|
@@ -17137,7 +17201,7 @@ var PostgresDriver = class {
|
|
|
17137
17201
|
* @returns The inserted document
|
|
17138
17202
|
*/
|
|
17139
17203
|
async insert(table, document, _options) {
|
|
17140
|
-
const serialized = this.serialize(document);
|
|
17204
|
+
const serialized = this.serialize(document, table);
|
|
17141
17205
|
const filteredData = Object.fromEntries(Object.entries(serialized).filter(([key, value]) => {
|
|
17142
17206
|
if (key === "id" && (value === null || value === void 0)) return false;
|
|
17143
17207
|
return true;
|
|
@@ -17164,7 +17228,7 @@ var PostgresDriver = class {
|
|
|
17164
17228
|
if (documents.length === 0) return [];
|
|
17165
17229
|
const allColumns = /* @__PURE__ */ new Set();
|
|
17166
17230
|
for (const doc of documents) {
|
|
17167
|
-
const serialized = this.serialize(doc);
|
|
17231
|
+
const serialized = this.serialize(doc, table);
|
|
17168
17232
|
Object.keys(serialized).forEach((key) => allColumns.add(key));
|
|
17169
17233
|
}
|
|
17170
17234
|
const columns = Array.from(allColumns);
|
|
@@ -17174,7 +17238,7 @@ var PostgresDriver = class {
|
|
|
17174
17238
|
const params = [];
|
|
17175
17239
|
let paramIndex = 1;
|
|
17176
17240
|
for (const doc of documents) {
|
|
17177
|
-
const serialized = this.serialize(doc);
|
|
17241
|
+
const serialized = this.serialize(doc, table);
|
|
17178
17242
|
const rowPlaceholders = [];
|
|
17179
17243
|
for (const col of columns) if (col in serialized) {
|
|
17180
17244
|
rowPlaceholders.push(this.dialect.placeholder(paramIndex++));
|
|
@@ -17241,7 +17305,7 @@ var PostgresDriver = class {
|
|
|
17241
17305
|
* @returns The replaced document or null
|
|
17242
17306
|
*/
|
|
17243
17307
|
async replace(table, filter, document, _options) {
|
|
17244
|
-
const serialized = this.serialize(document);
|
|
17308
|
+
const serialized = this.serialize(document, table);
|
|
17245
17309
|
const columns = Object.keys(serialized);
|
|
17246
17310
|
const values = Object.values(serialized);
|
|
17247
17311
|
const quotedTable = this.dialect.quoteIdentifier(table);
|
|
@@ -17263,7 +17327,7 @@ var PostgresDriver = class {
|
|
|
17263
17327
|
* @returns The upserted row
|
|
17264
17328
|
*/
|
|
17265
17329
|
async upsert(table, filter, document, options) {
|
|
17266
|
-
const serialized = this.serialize(document);
|
|
17330
|
+
const serialized = this.serialize(document, table);
|
|
17267
17331
|
const columns = Object.keys(serialized);
|
|
17268
17332
|
const values = Object.values(serialized);
|
|
17269
17333
|
if (columns.length === 0) throw new Error("Cannot upsert empty document");
|
|
@@ -17389,13 +17453,14 @@ var PostgresDriver = class {
|
|
|
17389
17453
|
* @throws {Error} If transaction fails or is explicitly rolled back
|
|
17390
17454
|
*/
|
|
17391
17455
|
async transaction(fn, options) {
|
|
17392
|
-
|
|
17456
|
+
const ctx = { rollback(reason) {
|
|
17457
|
+
throw new TransactionRollbackError(reason);
|
|
17458
|
+
} };
|
|
17459
|
+
if (databaseTransactionContext.hasActiveTransaction()) return fn(ctx);
|
|
17393
17460
|
const tx = await this.beginTransaction(options);
|
|
17394
17461
|
databaseTransactionContext.enter({ session: tx.context });
|
|
17395
17462
|
try {
|
|
17396
|
-
const result = await fn(
|
|
17397
|
-
throw new TransactionRollbackError(reason);
|
|
17398
|
-
} });
|
|
17463
|
+
const result = await fn(ctx);
|
|
17399
17464
|
await tx.commit();
|
|
17400
17465
|
return result;
|
|
17401
17466
|
} catch (error) {
|
|
@@ -17557,7 +17622,7 @@ var PostgresDriver = class {
|
|
|
17557
17622
|
let paramIndex = 1;
|
|
17558
17623
|
if (update.$set) for (const [key, value] of Object.entries(update.$set)) {
|
|
17559
17624
|
setClauses.push(`${this.dialect.quoteIdentifier(key)} = ${this.dialect.placeholder(paramIndex++)}`);
|
|
17560
|
-
params.push(value === void 0 ? value : this.serializeValue(key, value));
|
|
17625
|
+
params.push(value === void 0 ? value : this.serializeValue(key, value, table));
|
|
17561
17626
|
}
|
|
17562
17627
|
if (update.$unset) for (const key of Object.keys(update.$unset)) setClauses.push(`${this.dialect.quoteIdentifier(key)} = NULL`);
|
|
17563
17628
|
if (update.$inc) for (const [key, amount] of Object.entries(update.$inc)) {
|