@warlock.js/cascade 5.24.0 → 5.25.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/CHANGELOG.md +18 -5
- package/README.md +101 -101
- package/bin/cascade.js +0 -0
- package/cjs/index.cjs +201 -71
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/database-driver.contract.d.mts +22 -1
- package/esm/contracts/database-driver.contract.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +1 -1
- package/esm/data-source/data-source-registry.d.mts +8 -6
- package/esm/data-source/data-source-registry.d.mts.map +1 -1
- package/esm/data-source/data-source-registry.mjs +7 -1
- package/esm/data-source/data-source-registry.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts +13 -0
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +71 -2
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs +2 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts +9 -0
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +55 -7
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts +1 -4
- package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs +4 -5
- package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
- package/esm/errors/unsafe-filter.error.mjs.map +1 -1
- package/esm/errors/unsafe-raw-expression.error.mjs.map +1 -1
- package/esm/errors/unsupported-lock-for-update.error.d.mts +11 -0
- package/esm/errors/unsupported-lock-for-update.error.d.mts.map +1 -0
- package/esm/errors/unsupported-lock-for-update.error.mjs +16 -0
- package/esm/errors/unsupported-lock-for-update.error.mjs.map +1 -0
- package/esm/events/model-events.mjs.map +1 -1
- package/esm/index.d.mts +3 -2
- package/esm/index.mjs +2 -1
- package/esm/migration/migration-runner.mjs.map +1 -1
- package/esm/migration/sql-grammar.mjs.map +1 -1
- package/esm/model/generate-model-id.mjs +23 -0
- package/esm/model/generate-model-id.mjs.map +1 -0
- package/esm/model/methods/delete-methods.mjs.map +1 -1
- package/esm/model/methods/meta-methods.mjs.map +1 -1
- package/esm/model/methods/query-methods.mjs +25 -2
- package/esm/model/methods/query-methods.mjs.map +1 -1
- package/esm/model/methods/write-methods.mjs.map +1 -1
- package/esm/query-builder/query-builder.mjs +1 -1
- package/esm/query-builder/query-builder.mjs.map +1 -1
- package/esm/utils/sanitize-filter.mjs.map +1 -1
- package/esm/validation/plugins/database-rules-plugin.mjs.map +1 -1
- package/esm/writer/database-writer.d.mts +0 -33
- package/esm/writer/database-writer.d.mts.map +1 -1
- package/esm/writer/database-writer.mjs +3 -52
- package/esm/writer/database-writer.mjs.map +1 -1
- package/llms-full.txt +2901 -2864
- package/package.json +5 -5
- package/skills/aggregate-data/SKILL.md +165 -165
- package/skills/alter-migration/SKILL.md +531 -531
- package/skills/configure-delete-strategy/SKILL.md +130 -130
- package/skills/define-model/SKILL.md +205 -205
- package/skills/define-relations/SKILL.md +171 -171
- package/skills/manage-data-sources/SKILL.md +177 -140
- package/skills/manage-transactions/SKILL.md +215 -205
- package/skills/paginate-results/SKILL.md +122 -122
- package/skills/perform-atomic-ops/SKILL.md +156 -156
- package/skills/query-data/SKILL.md +213 -213
- package/skills/run-cascade-cli/SKILL.md +125 -125
- package/skills/search-by-vector/SKILL.md +127 -127
- package/skills/subscribe-to-model-events/SKILL.md +148 -148
- package/skills/track-changes/SKILL.md +127 -127
- package/skills/write-migration/SKILL.md +206 -206
|
@@ -1,140 +1,177 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: manage-data-sources
|
|
3
|
-
description: 'Configure multiple databases — register each via `connectToDatabase({ name, driver, database, isDefault })`, assign a model with `static dataSource = "name"`, route a migration with `dataSource` on the migration class, inspect via `dataSourceRegistry.get(name)` / `getAllDataSources()`. The first (or `isDefault: true`) source is the default. Triggers: `connectToDatabase`, `dataSourceRegistry`, `dataSourceRegistry.get`, `getAllDataSources`, `static dataSource`; "multi-database app", "per-tenant DB", "analytics on separate DB"; typical import `import { connectToDatabase, dataSourceRegistry } from "@warlock.js/cascade"`. Skip: per-source migrations — `@warlock.js/cascade/write-migration/SKILL.md`; transaction scope — `@warlock.js/cascade/manage-transactions/SKILL.md`; competing patterns `mongoose.createConnection`, `typeorm` `DataSource`, `prisma` multi-schema.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Manage data sources
|
|
7
|
-
|
|
8
|
-
Most apps run against a single database, and Cascade's defaults assume that. When you need multiple — analytics on a separate DB, per-tenant DBs — register each one and bind models to the right source.
|
|
9
|
-
|
|
10
|
-
## Register sources at boot
|
|
11
|
-
|
|
12
|
-
`connectToDatabase()` builds the driver, registers the data source, and connects. Call it once per database:
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
import { connectToDatabase } from "@warlock.js/cascade";
|
|
16
|
-
|
|
17
|
-
await connectToDatabase({
|
|
18
|
-
name: "primary",
|
|
19
|
-
driver: "postgres",
|
|
20
|
-
database: "app",
|
|
21
|
-
host: "localhost",
|
|
22
|
-
port: 5432,
|
|
23
|
-
username: "app",
|
|
24
|
-
password: "secret",
|
|
25
|
-
isDefault: true, // the default source for models that don't pin one
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
await connectToDatabase({
|
|
29
|
-
name: "analytics",
|
|
30
|
-
driver: "postgres",
|
|
31
|
-
database: "analytics",
|
|
32
|
-
host: "analytics-host",
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
await connectToDatabase({
|
|
36
|
-
name: "logs",
|
|
37
|
-
driver: "mongodb",
|
|
38
|
-
database: "logs",
|
|
39
|
-
uri: "mongodb://localhost:27017",
|
|
40
|
-
});
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The first source registered becomes the default unless another passes `isDefault: true`. (Lower-level: `dataSourceRegistry.register({ name, driver, isDefault })` if you build the driver yourself — `register` takes a `DataSourceOptions` object whose `driver` is a `DriverContract` instance, and constructs the `DataSource` for you.)
|
|
44
|
-
|
|
45
|
-
## Assign a model to a source
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
@RegisterModel()
|
|
49
|
-
export class AnalyticsEvent extends Model<AnalyticsEventSchema> {
|
|
50
|
-
public static table = "analytics_events";
|
|
51
|
-
public static schema = analyticsEventSchema;
|
|
52
|
-
public static dataSource = "analytics"; // ← goes to the analytics DB
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
@RegisterModel()
|
|
56
|
-
export class LogEntry extends Model<LogEntrySchema> {
|
|
57
|
-
public static table = "logs";
|
|
58
|
-
public static schema = logEntrySchema;
|
|
59
|
-
public static dataSource = "logs"; // ← goes to MongoDB
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
`static dataSource` takes the registered name (or a `DataSource` instance). Models without it use the default. Same query API; different storage. There is no per-query `Model.using(name)` override — the binding lives on the class.
|
|
64
|
-
|
|
65
|
-
## Migrations per data source
|
|
66
|
-
|
|
67
|
-
A migration class can pin its target source:
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
import { Migration } from "@warlock.js/cascade";
|
|
71
|
-
|
|
72
|
-
export default class CreateEventsTable extends Migration {
|
|
73
|
-
public readonly table = "analytics_events";
|
|
74
|
-
public readonly dataSource = "analytics";
|
|
75
|
-
|
|
76
|
-
public up(): void {
|
|
77
|
-
this.createTable();
|
|
78
|
-
this.id();
|
|
79
|
-
this.string("type");
|
|
80
|
-
this.timestamps();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
public down(): void {
|
|
84
|
-
this.dropTable();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
See [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) for the migration shape.
|
|
90
|
-
|
|
91
|
-
## Transactions and data sources
|
|
92
|
-
|
|
93
|
-
The top-level `transaction(fn)` helper runs on the **default** source's driver — it does not take a source name. To transact against a non-default source, reach for that source's driver directly (`dataSourceRegistry.get("analytics").driver.transaction(...)`). Transactions can't span two sources; coordinate at the application level (saga / outbox) if you need that.
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
await transaction(async () => {
|
|
97
|
-
// runs on the default source
|
|
98
|
-
await Order.create({ ... });
|
|
99
|
-
await OrderItem.createMany(items);
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## Multi-tenant per-tenant database
|
|
104
|
-
|
|
105
|
-
For strict tenant isolation, register a source per tenant once, then bind at the model layer (or resolve the model class per tenant):
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
async function ensureTenantSource(tenantId: string) {
|
|
109
|
-
try {
|
|
110
|
-
dataSourceRegistry.get(tenantId); // throws if not registered
|
|
111
|
-
} catch {
|
|
112
|
-
const config = await loadTenantConfig(tenantId);
|
|
113
|
-
await connectToDatabase({ name: tenantId, ...config, isDefault: false });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Register each tenant source once and reuse it — re-registering the same name re-creates the source.
|
|
119
|
-
|
|
120
|
-
## Inspection
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
dataSourceRegistry.get("analytics"); // the DataSource instance (throws if missing)
|
|
124
|
-
dataSourceRegistry.get(); // the default DataSource
|
|
125
|
-
dataSourceRegistry.getAllDataSources(); // DataSource[] — every registered source
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
There is no `has(name)` / `list()` / `setDefault()` — guard with a `try/catch` around `get(name)`, iterate `getAllDataSources()`, and set the default via `isDefault` at registration.
|
|
129
|
-
|
|
130
|
-
##
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
1
|
+
---
|
|
2
|
+
name: manage-data-sources
|
|
3
|
+
description: 'Configure multiple databases — register each via `connectToDatabase({ name, driver, database, isDefault })`, assign a model with `static dataSource = "name"`, route a migration with `dataSource` on the migration class, inspect via `dataSourceRegistry.get(name)` / `getAllDataSources()`. The first (or `isDefault: true`) source is the default. Triggers: `connectToDatabase`, `dataSourceRegistry`, `dataSourceRegistry.get`, `getAllDataSources`, `static dataSource`; "multi-database app", "per-tenant DB", "analytics on separate DB"; typical import `import { connectToDatabase, dataSourceRegistry } from "@warlock.js/cascade"`. Skip: per-source migrations — `@warlock.js/cascade/write-migration/SKILL.md`; transaction scope — `@warlock.js/cascade/manage-transactions/SKILL.md`; competing patterns `mongoose.createConnection`, `typeorm` `DataSource`, `prisma` multi-schema.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Manage data sources
|
|
7
|
+
|
|
8
|
+
Most apps run against a single database, and Cascade's defaults assume that. When you need multiple — analytics on a separate DB, per-tenant DBs — register each one and bind models to the right source.
|
|
9
|
+
|
|
10
|
+
## Register sources at boot
|
|
11
|
+
|
|
12
|
+
`connectToDatabase()` builds the driver, registers the data source, and connects. Call it once per database:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { connectToDatabase } from "@warlock.js/cascade";
|
|
16
|
+
|
|
17
|
+
await connectToDatabase({
|
|
18
|
+
name: "primary",
|
|
19
|
+
driver: "postgres",
|
|
20
|
+
database: "app",
|
|
21
|
+
host: "localhost",
|
|
22
|
+
port: 5432,
|
|
23
|
+
username: "app",
|
|
24
|
+
password: "secret",
|
|
25
|
+
isDefault: true, // the default source for models that don't pin one
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await connectToDatabase({
|
|
29
|
+
name: "analytics",
|
|
30
|
+
driver: "postgres",
|
|
31
|
+
database: "analytics",
|
|
32
|
+
host: "analytics-host",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await connectToDatabase({
|
|
36
|
+
name: "logs",
|
|
37
|
+
driver: "mongodb",
|
|
38
|
+
database: "logs",
|
|
39
|
+
uri: "mongodb://localhost:27017",
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The first source registered becomes the default unless another passes `isDefault: true`. (Lower-level: `dataSourceRegistry.register({ name, driver, isDefault })` if you build the driver yourself — `register` takes a `DataSourceOptions` object whose `driver` is a `DriverContract` instance, and constructs the `DataSource` for you.)
|
|
44
|
+
|
|
45
|
+
## Assign a model to a source
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
@RegisterModel()
|
|
49
|
+
export class AnalyticsEvent extends Model<AnalyticsEventSchema> {
|
|
50
|
+
public static table = "analytics_events";
|
|
51
|
+
public static schema = analyticsEventSchema;
|
|
52
|
+
public static dataSource = "analytics"; // ← goes to the analytics DB
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@RegisterModel()
|
|
56
|
+
export class LogEntry extends Model<LogEntrySchema> {
|
|
57
|
+
public static table = "logs";
|
|
58
|
+
public static schema = logEntrySchema;
|
|
59
|
+
public static dataSource = "logs"; // ← goes to MongoDB
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`static dataSource` takes the registered name (or a `DataSource` instance). Models without it use the default. Same query API; different storage. There is no per-query `Model.using(name)` override — the binding lives on the class.
|
|
64
|
+
|
|
65
|
+
## Migrations per data source
|
|
66
|
+
|
|
67
|
+
A migration class can pin its target source:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { Migration } from "@warlock.js/cascade";
|
|
71
|
+
|
|
72
|
+
export default class CreateEventsTable extends Migration {
|
|
73
|
+
public readonly table = "analytics_events";
|
|
74
|
+
public readonly dataSource = "analytics";
|
|
75
|
+
|
|
76
|
+
public up(): void {
|
|
77
|
+
this.createTable();
|
|
78
|
+
this.id();
|
|
79
|
+
this.string("type");
|
|
80
|
+
this.timestamps();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public down(): void {
|
|
84
|
+
this.dropTable();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) for the migration shape.
|
|
90
|
+
|
|
91
|
+
## Transactions and data sources
|
|
92
|
+
|
|
93
|
+
The top-level `transaction(fn)` helper runs on the **default** source's driver — it does not take a source name. To transact against a non-default source, reach for that source's driver directly (`dataSourceRegistry.get("analytics").driver.transaction(...)`). Transactions can't span two sources; coordinate at the application level (saga / outbox) if you need that.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
await transaction(async () => {
|
|
97
|
+
// runs on the default source
|
|
98
|
+
await Order.create({ ... });
|
|
99
|
+
await OrderItem.createMany(items);
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Multi-tenant per-tenant database
|
|
104
|
+
|
|
105
|
+
For strict tenant isolation, register a source per tenant once, then bind at the model layer (or resolve the model class per tenant):
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
async function ensureTenantSource(tenantId: string) {
|
|
109
|
+
try {
|
|
110
|
+
dataSourceRegistry.get(tenantId); // throws if not registered
|
|
111
|
+
} catch {
|
|
112
|
+
const config = await loadTenantConfig(tenantId);
|
|
113
|
+
await connectToDatabase({ name: tenantId, ...config, isDefault: false });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Register each tenant source once and reuse it — re-registering the same name re-creates the source.
|
|
119
|
+
|
|
120
|
+
## Inspection
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
dataSourceRegistry.get("analytics"); // the DataSource instance (throws if missing)
|
|
124
|
+
dataSourceRegistry.get(); // the default DataSource
|
|
125
|
+
dataSourceRegistry.getAllDataSources(); // DataSource[] — every registered source
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
There is no `has(name)` / `list()` / `setDefault()` — guard with a `try/catch` around `get(name)`, iterate `getAllDataSources()`, and set the default via `isDefault` at registration.
|
|
129
|
+
|
|
130
|
+
## Observe database queries
|
|
131
|
+
|
|
132
|
+
Subscribe once at the registry to observe settled queries from every registered
|
|
133
|
+
source. Keep the listener reference to remove it later:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { dataSourceRegistry, type QueryEvent } from "@warlock.js/cascade";
|
|
137
|
+
import { log } from "@warlock.js/logger";
|
|
138
|
+
|
|
139
|
+
const onQuery = (event: QueryEvent) => {
|
|
140
|
+
if (event.durationMs > 200) {
|
|
141
|
+
log.warn("database", "slow-query", event);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
dataSourceRegistry.on("query", onQuery);
|
|
146
|
+
// Later:
|
|
147
|
+
dataSourceRegistry.off("query", onQuery);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`QueryEvent` has `connection`, `driver` (`"postgres"` or `"mongodb"`),
|
|
151
|
+
`durationMs`, and `startedAt`; SQL events may include `sql`, `bindings`, and
|
|
152
|
+
`rowCount`, while Mongo events may include `collection`, `command`, and
|
|
153
|
+
`pipeline`. It also carries `error` when the command failed. Registry events
|
|
154
|
+
use the registered data-source name as `connection`.
|
|
155
|
+
|
|
156
|
+
For one source only, subscribe to its driver with the same listener lifecycle:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const source = dataSourceRegistry.get("analytics");
|
|
160
|
+
source.driver.on("query", onQuery);
|
|
161
|
+
source.driver.off("query", onQuery);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A throwing listener never fails the query. Mongo command monitoring is enabled
|
|
165
|
+
unless `clientOptions.monitorCommands` is explicitly set.
|
|
166
|
+
|
|
167
|
+
## Things NOT to do
|
|
168
|
+
|
|
169
|
+
- Don't call `dataSourceRegistry.register("name", config)` — `register` takes a single `DataSourceOptions` object (`{ name, driver, isDefault }`, `driver` being a built `DriverContract`); for the common case use `connectToDatabase({ name, ... })`.
|
|
170
|
+
- Don't reach for `Model.using(name)` / `setDefault` / `has` / `list` — they don't exist. Bind via `static dataSource`, inspect via `get` / `getAllDataSources`.
|
|
171
|
+
- Don't span a transaction across two data sources. Use a saga / outbox pattern.
|
|
172
|
+
- Don't write to a read replica. Most replicas reject writes; the data is overwritten on the next replication.
|
|
173
|
+
|
|
174
|
+
## See also
|
|
175
|
+
|
|
176
|
+
- [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) — per-source migrations
|
|
177
|
+
- [`@warlock.js/cascade/manage-transactions/SKILL.md`](@warlock.js/cascade/manage-transactions/SKILL.md) — transactions run on the default source
|