@getstrata/core 1.1.1 → 1.1.3
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 +13 -0
- package/dist/core/database/model.d.ts +4 -3
- package/dist/core/queue/index.d.ts +2 -0
- package/dist/entries/database/migrations.js +219 -5
- package/dist/entries/database/model.js +21 -2
- package/dist/entries/jobs/exportAuditLogsJob.js +44 -0
- package/dist/entries/jobs/invalidateCacheTagsJob.js +44 -0
- package/dist/entries/queue.js +44 -0
- package/dist/index.js +81 -32
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.3
|
|
4
|
+
|
|
5
|
+
- Eager with() arrays, OpenAPI mkdir, public-read/auth docs
|
|
6
|
+
|
|
7
|
+
## 1.1.2
|
|
8
|
+
|
|
9
|
+
- Product CLI helpers, file migrations, and job discovery.
|
|
10
|
+
|
|
11
|
+
## Unreleased
|
|
12
|
+
|
|
13
|
+
- `Job` tracks `static jobName` on construct so `queue.dispatch(new FooJob(), payload)` resolves a registry name.
|
|
14
|
+
- The core migration runner is dialect-aware (placeholders, upsert, returning, timestamp column) so sqlite and mysql product apps can use `framework_migrations`. Import it from `@getstrata/core/database/migrations`.
|
|
15
|
+
|
|
3
16
|
## 1.1.1
|
|
4
17
|
|
|
5
18
|
Fix generated app boot
|
|
@@ -14,6 +14,7 @@ interface ModelConstructor<TEntity extends object, PrimaryKey extends keyof TEnt
|
|
|
14
14
|
}
|
|
15
15
|
type AnyModel = Model<Record<string, unknown>, "id">;
|
|
16
16
|
type RelatedRef<TRelated extends object, RelatedKey extends keyof TRelated & string> = RelatedModelClass<TRelated, RelatedKey> | string | (() => RelatedModelClass<TRelated, RelatedKey>);
|
|
17
|
+
type RelationNameInput = string | readonly string[];
|
|
17
18
|
type ModelObserver = {
|
|
18
19
|
retrieved?: (model: AnyModel) => unknown;
|
|
19
20
|
creating?: (model: AnyModel) => unknown;
|
|
@@ -36,7 +37,7 @@ declare class ModelQuery {
|
|
|
36
37
|
readonly query: RepositoryQuery<Record<string, unknown>, "id">;
|
|
37
38
|
private readonly eager;
|
|
38
39
|
constructor(modelClass: object, query: RepositoryQuery<Record<string, unknown>, "id">);
|
|
39
|
-
with(...relations:
|
|
40
|
+
with(...relations: RelationNameInput[]): this;
|
|
40
41
|
where(input: QueryWhere<object> | ((builder: import("./whereBuilder.ts").WhereBuilder<object>) => void)): this;
|
|
41
42
|
orWhere(input: QueryWhere<object> | ((builder: import("./whereBuilder.ts").WhereBuilder<object>) => void)): this;
|
|
42
43
|
orderBy(orderBy: QueryOptions<object>["orderBy"]): this;
|
|
@@ -124,7 +125,7 @@ declare class Model<TEntity extends object, PrimaryKey extends keyof TEntity & s
|
|
|
124
125
|
static query(this: object): ModelQuery;
|
|
125
126
|
static newFromRecord(this: object, record: object, exists?: boolean): Model<Record<string, unknown>, "id">;
|
|
126
127
|
static create(this: object, attributes: Record<string, unknown>, forced?: Record<string, unknown>): Promise<Model<Record<string, unknown>, "id">>;
|
|
127
|
-
static with(this: object, ...relations:
|
|
128
|
+
static with(this: object, ...relations: RelationNameInput[]): ModelQuery;
|
|
128
129
|
static withTrashed(this: object): ModelQuery;
|
|
129
130
|
static onlyTrashed(this: object): ModelQuery;
|
|
130
131
|
static chunk(this: object, count: number, callback: (models: Array<Model<Record<string, unknown>, "id">>) => Promise<boolean | void>): Promise<void>;
|
|
@@ -173,7 +174,7 @@ declare class Model<TEntity extends object, PrimaryKey extends keyof TEntity & s
|
|
|
173
174
|
morphMany<TRelated extends object, RelatedKey extends keyof TRelated & string>(related: RelatedRef<TRelated, RelatedKey>, morphName: string, typeKey?: keyof TRelated & string, idKey?: keyof TRelated & string, morphType?: string): MorphManyRelationQuery<TEntity, PrimaryKey, TRelated, RelatedKey>;
|
|
174
175
|
morphOne<TRelated extends object, RelatedKey extends keyof TRelated & string>(related: RelatedRef<TRelated, RelatedKey>, morphName: string, typeKey?: keyof TRelated & string, idKey?: keyof TRelated & string, morphType?: string): MorphOneRelationQuery<TEntity, PrimaryKey, TRelated, RelatedKey>;
|
|
175
176
|
morphTo(relatedByType: Record<string, RelatedRef<Record<string, unknown>, "id">>, morphName?: string, typeKey?: keyof TEntity & string, idKey?: keyof TEntity & string): MorphToRelationQuery<TEntity, PrimaryKey>;
|
|
176
|
-
load(...names:
|
|
177
|
+
load(...names: RelationNameInput[]): Promise<this>;
|
|
177
178
|
loaded<T = unknown>(name: string): T | undefined;
|
|
178
179
|
setLoaded(name: string, value: unknown): this;
|
|
179
180
|
mergeAttributes(patch: Partial<TEntity>): this;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
type QueuePriority = "high" | "default" | "low";
|
|
2
2
|
declare abstract class Job<TPayload extends object = object> {
|
|
3
|
+
static readonly jobName?: string;
|
|
3
4
|
readonly maxAttempts?: number;
|
|
4
5
|
readonly backoffMs?: number;
|
|
5
6
|
readonly priority?: QueuePriority;
|
|
7
|
+
constructor();
|
|
6
8
|
abstract handle(payload: TPayload): Promise<void>;
|
|
7
9
|
}
|
|
8
10
|
interface Queue {
|
|
@@ -4,6 +4,198 @@ import { readdir } from "fs/promises";
|
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { pathToFileURL } from "url";
|
|
6
6
|
|
|
7
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
8
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
9
|
+
function createAsyncContextStore(key) {
|
|
10
|
+
const symbol = Symbol.for(key);
|
|
11
|
+
const globalRecord = globalThis;
|
|
12
|
+
const existing = globalRecord[symbol];
|
|
13
|
+
if (existing) {
|
|
14
|
+
return existing;
|
|
15
|
+
}
|
|
16
|
+
const store = new AsyncLocalStorage;
|
|
17
|
+
globalRecord[symbol] = store;
|
|
18
|
+
return store;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ../../src/core/database/schema/driver.ts
|
|
22
|
+
function normalizeConnectionName(connection) {
|
|
23
|
+
const normalized = connection.trim().toLowerCase();
|
|
24
|
+
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
25
|
+
return "pgsql";
|
|
26
|
+
}
|
|
27
|
+
if (normalized === "mysql" || normalized === "mariadb") {
|
|
28
|
+
return "mysql";
|
|
29
|
+
}
|
|
30
|
+
if (normalized === "sqlite") {
|
|
31
|
+
return "sqlite";
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
34
|
+
}
|
|
35
|
+
function resolveDriverFromUrl(url) {
|
|
36
|
+
const normalized = url.trim().toLowerCase();
|
|
37
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
38
|
+
return "pgsql";
|
|
39
|
+
}
|
|
40
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
41
|
+
return "mysql";
|
|
42
|
+
}
|
|
43
|
+
if (normalized.startsWith("sqlite:")) {
|
|
44
|
+
return "sqlite";
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function resolveDatabaseDriver(options = {}) {
|
|
49
|
+
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
50
|
+
if (connection) {
|
|
51
|
+
return normalizeConnectionName(connection);
|
|
52
|
+
}
|
|
53
|
+
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
54
|
+
const fromUrl = resolveDriverFromUrl(url);
|
|
55
|
+
if (fromUrl) {
|
|
56
|
+
return fromUrl;
|
|
57
|
+
}
|
|
58
|
+
return "pgsql";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ../../src/core/database/dialect.ts
|
|
62
|
+
function assertSafeIdentifier(identifier) {
|
|
63
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
64
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
65
|
+
}
|
|
66
|
+
return identifier;
|
|
67
|
+
}
|
|
68
|
+
function quoteIdentifierFor(dialect, column) {
|
|
69
|
+
return dialect.quoteIdentifier(column);
|
|
70
|
+
}
|
|
71
|
+
var postgresDialect = {
|
|
72
|
+
driver: "pgsql",
|
|
73
|
+
placeholder(index) {
|
|
74
|
+
return `$${index}`;
|
|
75
|
+
},
|
|
76
|
+
quoteIdentifier(identifier) {
|
|
77
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
78
|
+
},
|
|
79
|
+
nowExpression() {
|
|
80
|
+
return "NOW()";
|
|
81
|
+
},
|
|
82
|
+
timestampValue(value) {
|
|
83
|
+
return value.toISOString();
|
|
84
|
+
},
|
|
85
|
+
returningClause(columns) {
|
|
86
|
+
return ` RETURNING ${columns}`;
|
|
87
|
+
},
|
|
88
|
+
ilikeOperator() {
|
|
89
|
+
return "ILIKE";
|
|
90
|
+
},
|
|
91
|
+
nullsLastSuffix() {
|
|
92
|
+
return " NULLS LAST";
|
|
93
|
+
},
|
|
94
|
+
castToText(expression) {
|
|
95
|
+
return `${expression}::text`;
|
|
96
|
+
},
|
|
97
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
98
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
99
|
+
if (updateColumns.length === 0) {
|
|
100
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
101
|
+
}
|
|
102
|
+
const assignments = updateColumns.map((column) => {
|
|
103
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
104
|
+
return `${quoted} = excluded.${quoted}`;
|
|
105
|
+
}).join(", ");
|
|
106
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var mysqlDialect = {
|
|
110
|
+
driver: "mysql",
|
|
111
|
+
placeholder() {
|
|
112
|
+
return "?";
|
|
113
|
+
},
|
|
114
|
+
quoteIdentifier(identifier) {
|
|
115
|
+
return `\`${assertSafeIdentifier(identifier)}\``;
|
|
116
|
+
},
|
|
117
|
+
nowExpression() {
|
|
118
|
+
return "CURRENT_TIMESTAMP";
|
|
119
|
+
},
|
|
120
|
+
timestampValue(value) {
|
|
121
|
+
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
122
|
+
},
|
|
123
|
+
returningClause() {
|
|
124
|
+
return "";
|
|
125
|
+
},
|
|
126
|
+
ilikeOperator() {
|
|
127
|
+
return "LIKE";
|
|
128
|
+
},
|
|
129
|
+
nullsLastSuffix() {
|
|
130
|
+
return "";
|
|
131
|
+
},
|
|
132
|
+
castToText(expression) {
|
|
133
|
+
return `CAST(${expression} AS CHAR)`;
|
|
134
|
+
},
|
|
135
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
136
|
+
if (updateColumns.length === 0) {
|
|
137
|
+
const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
|
|
138
|
+
return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
|
|
139
|
+
}
|
|
140
|
+
const assignments = updateColumns.map((column) => {
|
|
141
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
142
|
+
return `${quoted} = VALUES(${quoted})`;
|
|
143
|
+
}).join(", ");
|
|
144
|
+
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var sqliteDialect = {
|
|
148
|
+
driver: "sqlite",
|
|
149
|
+
placeholder() {
|
|
150
|
+
return "?";
|
|
151
|
+
},
|
|
152
|
+
quoteIdentifier(identifier) {
|
|
153
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
154
|
+
},
|
|
155
|
+
nowExpression() {
|
|
156
|
+
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
157
|
+
},
|
|
158
|
+
timestampValue(value) {
|
|
159
|
+
return value.toISOString();
|
|
160
|
+
},
|
|
161
|
+
returningClause(columns) {
|
|
162
|
+
return ` RETURNING ${columns}`;
|
|
163
|
+
},
|
|
164
|
+
ilikeOperator() {
|
|
165
|
+
return "LIKE";
|
|
166
|
+
},
|
|
167
|
+
nullsLastSuffix() {
|
|
168
|
+
return "";
|
|
169
|
+
},
|
|
170
|
+
castToText(expression) {
|
|
171
|
+
return `CAST(${expression} AS TEXT)`;
|
|
172
|
+
},
|
|
173
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
174
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
175
|
+
if (updateColumns.length === 0) {
|
|
176
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
177
|
+
}
|
|
178
|
+
const assignments = updateColumns.map((column) => {
|
|
179
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
180
|
+
return `${quoted} = excluded.${quoted}`;
|
|
181
|
+
}).join(", ");
|
|
182
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var dialects = {
|
|
186
|
+
pgsql: postgresDialect,
|
|
187
|
+
mysql: mysqlDialect,
|
|
188
|
+
sqlite: sqliteDialect
|
|
189
|
+
};
|
|
190
|
+
var dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
191
|
+
var dialectOverride = null;
|
|
192
|
+
function dialectFor(driver) {
|
|
193
|
+
return dialects[driver];
|
|
194
|
+
}
|
|
195
|
+
function currentSqlDialect() {
|
|
196
|
+
return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
|
|
197
|
+
}
|
|
198
|
+
|
|
7
199
|
// ../../src/core/database/migrations/advisoryLock.ts
|
|
8
200
|
var MIGRATION_LOCK_KEY = 42424242;
|
|
9
201
|
async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
|
|
@@ -17,15 +209,37 @@ async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
|
|
|
17
209
|
|
|
18
210
|
// ../../src/core/database/migrations/runner.ts
|
|
19
211
|
var MIGRATIONS_TABLE = "framework_migrations";
|
|
212
|
+
function migrationsNameColumn() {
|
|
213
|
+
return currentSqlDialect().driver === "mysql" ? "VARCHAR(255) PRIMARY KEY" : "TEXT PRIMARY KEY";
|
|
214
|
+
}
|
|
215
|
+
function migrationsRunOnColumn() {
|
|
216
|
+
const dialect = currentSqlDialect();
|
|
217
|
+
if (dialect.driver === "pgsql") {
|
|
218
|
+
return `TIMESTAMPTZ NOT NULL DEFAULT ${dialect.nowExpression()}`;
|
|
219
|
+
}
|
|
220
|
+
if (dialect.driver === "mysql") {
|
|
221
|
+
return `DATETIME NOT NULL DEFAULT ${dialect.nowExpression()}`;
|
|
222
|
+
}
|
|
223
|
+
return "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP";
|
|
224
|
+
}
|
|
20
225
|
async function ensureMigrationsTable(db) {
|
|
21
226
|
await db.unsafe(`
|
|
22
227
|
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
23
|
-
name
|
|
228
|
+
name ${migrationsNameColumn()},
|
|
24
229
|
batch INTEGER NOT NULL,
|
|
25
|
-
run_on
|
|
230
|
+
run_on ${migrationsRunOnColumn()}
|
|
26
231
|
)
|
|
27
232
|
`);
|
|
28
233
|
}
|
|
234
|
+
async function recordAppliedMigration(db, name, batch) {
|
|
235
|
+
const dialect = currentSqlDialect();
|
|
236
|
+
const inserted = await db.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES (${dialect.placeholder(1)}, ${dialect.placeholder(2)})${dialect.upsertSuffix(["name"], [])}${dialect.returningClause("name")}`, [name, batch]);
|
|
237
|
+
if (dialect.returningClause("name")) {
|
|
238
|
+
return inserted.length > 0;
|
|
239
|
+
}
|
|
240
|
+
const rows = await db.unsafe(`SELECT batch FROM ${MIGRATIONS_TABLE} WHERE name = ${dialect.placeholder(1)}`, [name]);
|
|
241
|
+
return rows.some((row) => Number(row.batch) === batch);
|
|
242
|
+
}
|
|
29
243
|
async function getAppliedMigrations(db) {
|
|
30
244
|
await ensureMigrationsTable(db);
|
|
31
245
|
return await db.unsafe(`
|
|
@@ -61,8 +275,8 @@ async function runPendingMigrations(db, migrations, options = {}) {
|
|
|
61
275
|
for (const migration of pendingMigrations) {
|
|
62
276
|
options.onMigration?.(migration.name);
|
|
63
277
|
await migration.up(db);
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
278
|
+
const recorded = await recordAppliedMigration(db, migration.name, nextBatch);
|
|
279
|
+
if (!recorded) {
|
|
66
280
|
throw new Error(`Migration ${migration.name} was applied but not recorded.`);
|
|
67
281
|
}
|
|
68
282
|
}
|
|
@@ -89,7 +303,7 @@ async function rollbackDatabase(db, migrations, options = {}) {
|
|
|
89
303
|
}
|
|
90
304
|
options.onMigration?.(migration.name);
|
|
91
305
|
await migration.down(db);
|
|
92
|
-
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
|
|
306
|
+
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = ${currentSqlDialect().placeholder(1)}`, [migration.name]);
|
|
93
307
|
rolledBack += 1;
|
|
94
308
|
}
|
|
95
309
|
return rolledBack;
|
|
@@ -1469,6 +1469,25 @@ var namedModels = new Map;
|
|
|
1469
1469
|
var modelGlobalScopes = new WeakMap;
|
|
1470
1470
|
var modelObservers = new WeakMap;
|
|
1471
1471
|
var modelBooted = new WeakSet;
|
|
1472
|
+
function flattenRelationNames(relations) {
|
|
1473
|
+
const names = [];
|
|
1474
|
+
for (const item of relations) {
|
|
1475
|
+
if (typeof item === "string") {
|
|
1476
|
+
if (item.length > 0) {
|
|
1477
|
+
names.push(item);
|
|
1478
|
+
}
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
if (Array.isArray(item)) {
|
|
1482
|
+
for (const nested of item) {
|
|
1483
|
+
if (typeof nested === "string" && nested.length > 0) {
|
|
1484
|
+
names.push(nested);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return names;
|
|
1490
|
+
}
|
|
1472
1491
|
async function runObservers(model, hook) {
|
|
1473
1492
|
const observers = modelObservers.get(model.constructor) ?? [];
|
|
1474
1493
|
for (const observer of observers) {
|
|
@@ -1725,7 +1744,7 @@ class ModelQuery {
|
|
|
1725
1744
|
const statics = modelStatics(this.modelClass);
|
|
1726
1745
|
ensureBooted(this.modelClass);
|
|
1727
1746
|
const dummy = statics.newFromRecord({}, false);
|
|
1728
|
-
for (const path of relations) {
|
|
1747
|
+
for (const path of flattenRelationNames(relations)) {
|
|
1729
1748
|
const name = path.split(".")[0] ?? path;
|
|
1730
1749
|
const method = dummy[name];
|
|
1731
1750
|
if (typeof method !== "function") {
|
|
@@ -2387,7 +2406,7 @@ class Model {
|
|
|
2387
2406
|
}));
|
|
2388
2407
|
}
|
|
2389
2408
|
async load(...names) {
|
|
2390
|
-
for (const name of names) {
|
|
2409
|
+
for (const name of flattenRelationNames(names)) {
|
|
2391
2410
|
if (name.includes(".")) {
|
|
2392
2411
|
await loadNested(this, name);
|
|
2393
2412
|
continue;
|
|
@@ -615,11 +615,55 @@ class Logger {
|
|
|
615
615
|
}
|
|
616
616
|
var appLogger = new Logger("app");
|
|
617
617
|
|
|
618
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
619
|
+
class JobRegistry {
|
|
620
|
+
factories = new Map;
|
|
621
|
+
instances = new WeakMap;
|
|
622
|
+
register(name, factory) {
|
|
623
|
+
this.factories.set(name, factory);
|
|
624
|
+
}
|
|
625
|
+
resolveName(job) {
|
|
626
|
+
return this.instances.get(job);
|
|
627
|
+
}
|
|
628
|
+
track(name, job) {
|
|
629
|
+
this.instances.set(job, name);
|
|
630
|
+
return job;
|
|
631
|
+
}
|
|
632
|
+
create(name) {
|
|
633
|
+
const factory = this.factories.get(name);
|
|
634
|
+
if (!factory) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
return factory();
|
|
638
|
+
}
|
|
639
|
+
names() {
|
|
640
|
+
return [...this.factories.keys()];
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
644
|
+
function readSharedJobRegistry() {
|
|
645
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
646
|
+
if (globalRegistry) {
|
|
647
|
+
return globalRegistry;
|
|
648
|
+
}
|
|
649
|
+
const registry = new JobRegistry;
|
|
650
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
651
|
+
return registry;
|
|
652
|
+
}
|
|
653
|
+
var jobRegistry = readSharedJobRegistry();
|
|
654
|
+
|
|
618
655
|
// ../../src/core/queue/index.ts
|
|
619
656
|
class Job {
|
|
657
|
+
static jobName;
|
|
620
658
|
maxAttempts;
|
|
621
659
|
backoffMs;
|
|
622
660
|
priority;
|
|
661
|
+
constructor() {
|
|
662
|
+
const jobName = this.constructor.jobName;
|
|
663
|
+
if (jobName) {
|
|
664
|
+
jobRegistry.track(jobName, this);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
623
667
|
}
|
|
624
668
|
|
|
625
669
|
class SyncQueue {
|
|
@@ -1,9 +1,53 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
3
|
+
class JobRegistry {
|
|
4
|
+
factories = new Map;
|
|
5
|
+
instances = new WeakMap;
|
|
6
|
+
register(name, factory) {
|
|
7
|
+
this.factories.set(name, factory);
|
|
8
|
+
}
|
|
9
|
+
resolveName(job) {
|
|
10
|
+
return this.instances.get(job);
|
|
11
|
+
}
|
|
12
|
+
track(name, job) {
|
|
13
|
+
this.instances.set(job, name);
|
|
14
|
+
return job;
|
|
15
|
+
}
|
|
16
|
+
create(name) {
|
|
17
|
+
const factory = this.factories.get(name);
|
|
18
|
+
if (!factory) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
return factory();
|
|
22
|
+
}
|
|
23
|
+
names() {
|
|
24
|
+
return [...this.factories.keys()];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
28
|
+
function readSharedJobRegistry() {
|
|
29
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
30
|
+
if (globalRegistry) {
|
|
31
|
+
return globalRegistry;
|
|
32
|
+
}
|
|
33
|
+
const registry = new JobRegistry;
|
|
34
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
35
|
+
return registry;
|
|
36
|
+
}
|
|
37
|
+
var jobRegistry = readSharedJobRegistry();
|
|
38
|
+
|
|
2
39
|
// ../../src/core/queue/index.ts
|
|
3
40
|
class Job {
|
|
41
|
+
static jobName;
|
|
4
42
|
maxAttempts;
|
|
5
43
|
backoffMs;
|
|
6
44
|
priority;
|
|
45
|
+
constructor() {
|
|
46
|
+
const jobName = this.constructor.jobName;
|
|
47
|
+
if (jobName) {
|
|
48
|
+
jobRegistry.track(jobName, this);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
7
51
|
}
|
|
8
52
|
|
|
9
53
|
class SyncQueue {
|
package/dist/entries/queue.js
CHANGED
|
@@ -1,9 +1,53 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
3
|
+
class JobRegistry {
|
|
4
|
+
factories = new Map;
|
|
5
|
+
instances = new WeakMap;
|
|
6
|
+
register(name, factory) {
|
|
7
|
+
this.factories.set(name, factory);
|
|
8
|
+
}
|
|
9
|
+
resolveName(job) {
|
|
10
|
+
return this.instances.get(job);
|
|
11
|
+
}
|
|
12
|
+
track(name, job) {
|
|
13
|
+
this.instances.set(job, name);
|
|
14
|
+
return job;
|
|
15
|
+
}
|
|
16
|
+
create(name) {
|
|
17
|
+
const factory = this.factories.get(name);
|
|
18
|
+
if (!factory) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
return factory();
|
|
22
|
+
}
|
|
23
|
+
names() {
|
|
24
|
+
return [...this.factories.keys()];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
28
|
+
function readSharedJobRegistry() {
|
|
29
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
30
|
+
if (globalRegistry) {
|
|
31
|
+
return globalRegistry;
|
|
32
|
+
}
|
|
33
|
+
const registry = new JobRegistry;
|
|
34
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
35
|
+
return registry;
|
|
36
|
+
}
|
|
37
|
+
var jobRegistry = readSharedJobRegistry();
|
|
38
|
+
|
|
2
39
|
// ../../src/core/queue/index.ts
|
|
3
40
|
class Job {
|
|
41
|
+
static jobName;
|
|
4
42
|
maxAttempts;
|
|
5
43
|
backoffMs;
|
|
6
44
|
priority;
|
|
45
|
+
constructor() {
|
|
46
|
+
const jobName = this.constructor.jobName;
|
|
47
|
+
if (jobName) {
|
|
48
|
+
jobRegistry.track(jobName, this);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
7
51
|
}
|
|
8
52
|
|
|
9
53
|
class SyncQueue {
|
package/dist/index.js
CHANGED
|
@@ -5082,15 +5082,37 @@ import { readdir } from "fs/promises";
|
|
|
5082
5082
|
import { join } from "path";
|
|
5083
5083
|
import { pathToFileURL } from "url";
|
|
5084
5084
|
var MIGRATIONS_TABLE = "framework_migrations";
|
|
5085
|
+
function migrationsNameColumn() {
|
|
5086
|
+
return currentSqlDialect().driver === "mysql" ? "VARCHAR(255) PRIMARY KEY" : "TEXT PRIMARY KEY";
|
|
5087
|
+
}
|
|
5088
|
+
function migrationsRunOnColumn() {
|
|
5089
|
+
const dialect = currentSqlDialect();
|
|
5090
|
+
if (dialect.driver === "pgsql") {
|
|
5091
|
+
return `TIMESTAMPTZ NOT NULL DEFAULT ${dialect.nowExpression()}`;
|
|
5092
|
+
}
|
|
5093
|
+
if (dialect.driver === "mysql") {
|
|
5094
|
+
return `DATETIME NOT NULL DEFAULT ${dialect.nowExpression()}`;
|
|
5095
|
+
}
|
|
5096
|
+
return "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP";
|
|
5097
|
+
}
|
|
5085
5098
|
async function ensureMigrationsTable(db) {
|
|
5086
5099
|
await db.unsafe(`
|
|
5087
5100
|
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
5088
|
-
name
|
|
5101
|
+
name ${migrationsNameColumn()},
|
|
5089
5102
|
batch INTEGER NOT NULL,
|
|
5090
|
-
run_on
|
|
5103
|
+
run_on ${migrationsRunOnColumn()}
|
|
5091
5104
|
)
|
|
5092
5105
|
`);
|
|
5093
5106
|
}
|
|
5107
|
+
async function recordAppliedMigration(db, name, batch) {
|
|
5108
|
+
const dialect = currentSqlDialect();
|
|
5109
|
+
const inserted = await db.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES (${dialect.placeholder(1)}, ${dialect.placeholder(2)})${dialect.upsertSuffix(["name"], [])}${dialect.returningClause("name")}`, [name, batch]);
|
|
5110
|
+
if (dialect.returningClause("name")) {
|
|
5111
|
+
return inserted.length > 0;
|
|
5112
|
+
}
|
|
5113
|
+
const rows = await db.unsafe(`SELECT batch FROM ${MIGRATIONS_TABLE} WHERE name = ${dialect.placeholder(1)}`, [name]);
|
|
5114
|
+
return rows.some((row) => Number(row.batch) === batch);
|
|
5115
|
+
}
|
|
5094
5116
|
async function getAppliedMigrations(db) {
|
|
5095
5117
|
await ensureMigrationsTable(db);
|
|
5096
5118
|
return await db.unsafe(`
|
|
@@ -5126,8 +5148,8 @@ async function runPendingMigrations(db, migrations, options = {}) {
|
|
|
5126
5148
|
for (const migration of pendingMigrations) {
|
|
5127
5149
|
options.onMigration?.(migration.name);
|
|
5128
5150
|
await migration.up(db);
|
|
5129
|
-
const
|
|
5130
|
-
if (
|
|
5151
|
+
const recorded = await recordAppliedMigration(db, migration.name, nextBatch);
|
|
5152
|
+
if (!recorded) {
|
|
5131
5153
|
throw new Error(`Migration ${migration.name} was applied but not recorded.`);
|
|
5132
5154
|
}
|
|
5133
5155
|
}
|
|
@@ -5154,7 +5176,7 @@ async function rollbackDatabase(db, migrations, options = {}) {
|
|
|
5154
5176
|
}
|
|
5155
5177
|
options.onMigration?.(migration.name);
|
|
5156
5178
|
await migration.down(db);
|
|
5157
|
-
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
|
|
5179
|
+
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = ${currentSqlDialect().placeholder(1)}`, [migration.name]);
|
|
5158
5180
|
rolledBack += 1;
|
|
5159
5181
|
}
|
|
5160
5182
|
return rolledBack;
|
|
@@ -5846,6 +5868,25 @@ var namedModels = new Map;
|
|
|
5846
5868
|
var modelGlobalScopes = new WeakMap;
|
|
5847
5869
|
var modelObservers = new WeakMap;
|
|
5848
5870
|
var modelBooted = new WeakSet;
|
|
5871
|
+
function flattenRelationNames(relations) {
|
|
5872
|
+
const names = [];
|
|
5873
|
+
for (const item of relations) {
|
|
5874
|
+
if (typeof item === "string") {
|
|
5875
|
+
if (item.length > 0) {
|
|
5876
|
+
names.push(item);
|
|
5877
|
+
}
|
|
5878
|
+
continue;
|
|
5879
|
+
}
|
|
5880
|
+
if (Array.isArray(item)) {
|
|
5881
|
+
for (const nested of item) {
|
|
5882
|
+
if (typeof nested === "string" && nested.length > 0) {
|
|
5883
|
+
names.push(nested);
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
return names;
|
|
5889
|
+
}
|
|
5849
5890
|
async function runObservers(model, hook) {
|
|
5850
5891
|
const observers = modelObservers.get(model.constructor) ?? [];
|
|
5851
5892
|
for (const observer of observers) {
|
|
@@ -6102,7 +6143,7 @@ class ModelQuery {
|
|
|
6102
6143
|
const statics = modelStatics(this.modelClass);
|
|
6103
6144
|
ensureBooted(this.modelClass);
|
|
6104
6145
|
const dummy = statics.newFromRecord({}, false);
|
|
6105
|
-
for (const path of relations) {
|
|
6146
|
+
for (const path of flattenRelationNames(relations)) {
|
|
6106
6147
|
const name = path.split(".")[0] ?? path;
|
|
6107
6148
|
const method = dummy[name];
|
|
6108
6149
|
if (typeof method !== "function") {
|
|
@@ -6764,7 +6805,7 @@ class Model {
|
|
|
6764
6805
|
}));
|
|
6765
6806
|
}
|
|
6766
6807
|
async load(...names) {
|
|
6767
|
-
for (const name of names) {
|
|
6808
|
+
for (const name of flattenRelationNames(names)) {
|
|
6768
6809
|
if (name.includes(".")) {
|
|
6769
6810
|
await loadNested(this, name);
|
|
6770
6811
|
continue;
|
|
@@ -10387,31 +10428,6 @@ class Notification {
|
|
|
10387
10428
|
return null;
|
|
10388
10429
|
}
|
|
10389
10430
|
}
|
|
10390
|
-
// ../../src/core/queue/index.ts
|
|
10391
|
-
class Job {
|
|
10392
|
-
maxAttempts;
|
|
10393
|
-
backoffMs;
|
|
10394
|
-
priority;
|
|
10395
|
-
}
|
|
10396
|
-
|
|
10397
|
-
class SyncQueue {
|
|
10398
|
-
async dispatch(job, payload) {
|
|
10399
|
-
await job.handle(payload);
|
|
10400
|
-
}
|
|
10401
|
-
}
|
|
10402
|
-
|
|
10403
|
-
class AsyncQueue {
|
|
10404
|
-
async dispatch(job, payload) {
|
|
10405
|
-
setTimeout(() => {
|
|
10406
|
-
job.handle(payload).catch((error) => {
|
|
10407
|
-
console.error("[AsyncQueue] Job failed:", error);
|
|
10408
|
-
});
|
|
10409
|
-
}, 0);
|
|
10410
|
-
}
|
|
10411
|
-
}
|
|
10412
|
-
function createQueue(driver) {
|
|
10413
|
-
return driver === "async" ? new AsyncQueue : new SyncQueue;
|
|
10414
|
-
}
|
|
10415
10431
|
// ../../src/core/queue/jobRegistry.ts
|
|
10416
10432
|
class JobRegistry {
|
|
10417
10433
|
factories = new Map;
|
|
@@ -10448,6 +10464,39 @@ function readSharedJobRegistry() {
|
|
|
10448
10464
|
return registry;
|
|
10449
10465
|
}
|
|
10450
10466
|
var jobRegistry = readSharedJobRegistry();
|
|
10467
|
+
|
|
10468
|
+
// ../../src/core/queue/index.ts
|
|
10469
|
+
class Job {
|
|
10470
|
+
static jobName;
|
|
10471
|
+
maxAttempts;
|
|
10472
|
+
backoffMs;
|
|
10473
|
+
priority;
|
|
10474
|
+
constructor() {
|
|
10475
|
+
const jobName = this.constructor.jobName;
|
|
10476
|
+
if (jobName) {
|
|
10477
|
+
jobRegistry.track(jobName, this);
|
|
10478
|
+
}
|
|
10479
|
+
}
|
|
10480
|
+
}
|
|
10481
|
+
|
|
10482
|
+
class SyncQueue {
|
|
10483
|
+
async dispatch(job, payload) {
|
|
10484
|
+
await job.handle(payload);
|
|
10485
|
+
}
|
|
10486
|
+
}
|
|
10487
|
+
|
|
10488
|
+
class AsyncQueue {
|
|
10489
|
+
async dispatch(job, payload) {
|
|
10490
|
+
setTimeout(() => {
|
|
10491
|
+
job.handle(payload).catch((error) => {
|
|
10492
|
+
console.error("[AsyncQueue] Job failed:", error);
|
|
10493
|
+
});
|
|
10494
|
+
}, 0);
|
|
10495
|
+
}
|
|
10496
|
+
}
|
|
10497
|
+
function createQueue(driver) {
|
|
10498
|
+
return driver === "async" ? new AsyncQueue : new SyncQueue;
|
|
10499
|
+
}
|
|
10451
10500
|
// ../../src/core/queue/failedJobTable.ts
|
|
10452
10501
|
var failedJobTable = defineTable({
|
|
10453
10502
|
name: "failed_job",
|