@stacksjs/database 0.70.229 → 0.70.231
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/custom/jobs.js +36 -6
- package/dist/drivers/defaults/traits.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/managed-columns.d.ts +26 -3
- package/dist/managed-columns.js +19 -6
- package/dist/migrations.d.ts +67 -5
- package/dist/migrations.js +268 -20
- package/dist/notification-tables.d.ts +6 -0
- package/dist/notification-tables.js +24 -1
- package/dist/query-logger.d.ts +1 -0
- package/dist/query-logger.js +32 -5
- package/dist/relation-columns.d.ts +19 -0
- package/dist/relation-columns.js +66 -0
- package/dist/seeder.d.ts +29 -0
- package/dist/seeder.js +31 -11
- package/dist/utils.d.ts +24 -0
- package/dist/utils.js +35 -7
- package/package.json +10 -10
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getTableName } from "@stacksjs/orm";
|
|
2
|
+
import { path } from "@stacksjs/path";
|
|
3
|
+
import { fs } from "@stacksjs/storage";
|
|
4
|
+
function snakeCase(str) {
|
|
5
|
+
return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/(\d)([A-Za-z])/g, "$1_$2").toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
export function belongsToColumn(entry) {
|
|
8
|
+
if (typeof entry === "string")
|
|
9
|
+
return entry.length > 0 ? `${snakeCase(entry)}_id` : null;
|
|
10
|
+
if (entry && typeof entry === "object") {
|
|
11
|
+
const relation = entry;
|
|
12
|
+
if (typeof relation.foreignKey === "string" && relation.foreignKey.length > 0)
|
|
13
|
+
return relation.foreignKey;
|
|
14
|
+
if (typeof relation.model === "string" && relation.model.length > 0)
|
|
15
|
+
return `${snakeCase(relation.model)}_id`;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export function belongsToColumnsOf(model) {
|
|
20
|
+
const declared = model.belongsTo;
|
|
21
|
+
if (!declared)
|
|
22
|
+
return [];
|
|
23
|
+
const entries = Array.isArray(declared) ? declared : Object.entries(declared).map(([model, value]) => value && typeof value === "object" ? { model, ...value } : model), columns = [];
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const column = belongsToColumn(entry);
|
|
26
|
+
if (column)
|
|
27
|
+
columns.push(column);
|
|
28
|
+
}
|
|
29
|
+
return columns;
|
|
30
|
+
}
|
|
31
|
+
async function loadModelsFrom(dir) {
|
|
32
|
+
const out = [];
|
|
33
|
+
if (!fs.existsSync(dir))
|
|
34
|
+
return out;
|
|
35
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: !0 })) {
|
|
36
|
+
const fullPath = path.join(dir, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
out.push(...await loadModelsFrom(fullPath));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!entry.name.endsWith(".ts"))
|
|
42
|
+
continue;
|
|
43
|
+
if (entry.name.startsWith("_") || entry.name.startsWith("index"))
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
const imported = (await import(fullPath)).default;
|
|
47
|
+
if (imported?.name || imported?.table)
|
|
48
|
+
out.push({ filePath: fullPath, model: imported });
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
export async function findRelationForeignKeys() {
|
|
54
|
+
const dirs = [path.userModelsPath(), path.frameworkPath("defaults/app/Models")], byTable = new Map;
|
|
55
|
+
for (const dir of dirs)
|
|
56
|
+
for (const { filePath, model } of await loadModelsFrom(dir)) {
|
|
57
|
+
const columns = belongsToColumnsOf(model);
|
|
58
|
+
if (columns.length === 0)
|
|
59
|
+
continue;
|
|
60
|
+
const table = getTableName(model, filePath), existing = byTable.get(table) ?? new Set;
|
|
61
|
+
for (const column of columns)
|
|
62
|
+
existing.add(column);
|
|
63
|
+
byTable.set(table, existing);
|
|
64
|
+
}
|
|
65
|
+
return byTable;
|
|
66
|
+
}
|
package/dist/seeder.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Attribute, Model } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the path to the framework default models directory
|
|
4
|
+
*/
|
|
5
|
+
export declare function defaultModelsPath(subpath?: string): string;
|
|
2
6
|
/** Test whether a model holds accounts rather than fixtures. */
|
|
3
7
|
export declare function isAccountModel(name: string): boolean;
|
|
4
8
|
/**
|
|
@@ -7,6 +11,16 @@ export declare function isAccountModel(name: string): boolean;
|
|
|
7
11
|
* list stays a single source of truth.
|
|
8
12
|
*/
|
|
9
13
|
export declare function isProtectedModel(name: string): boolean;
|
|
14
|
+
/** Record the table names of the models about to be seeded. */
|
|
15
|
+
export declare function registerModelTables(models: { name: string, table: string }[]): void;
|
|
16
|
+
/**
|
|
17
|
+
* The table a parent model lives in.
|
|
18
|
+
*
|
|
19
|
+
* Falls back to the old guess for a parent that is not itself being seeded
|
|
20
|
+
* (a framework model with no `useSeeder`, say), which is still right for the
|
|
21
|
+
* regular plurals that make up most of them.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parentTable(parent: string): string;
|
|
10
24
|
/**
|
|
11
25
|
* Choose one consistent set of parent ids per record.
|
|
12
26
|
*
|
|
@@ -15,6 +29,16 @@ export declare function isProtectedModel(name: string): boolean;
|
|
|
15
29
|
* foreign keys agree with one another.
|
|
16
30
|
*/
|
|
17
31
|
export declare function chooseRelations(pools: { column: string, rows: Record<string, unknown>[] }[], count: number): Record<string, unknown>[];
|
|
32
|
+
/**
|
|
33
|
+
* The parents a model declares, with the column each one is reached through.
|
|
34
|
+
*
|
|
35
|
+
* The column defaults to `<model>_id`, which is what most relations look like.
|
|
36
|
+
* An entry may override it with `foreignKey`, and doing so is not a niceness:
|
|
37
|
+
* a model that belongs to `User` twice — an author and a reviewer — has no
|
|
38
|
+
* `user_id` at all, so under the derived name neither key was ever filled and
|
|
39
|
+
* both fell back to whatever the factory invented.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parentRelations(model: SeederModel): ParentRelation[];
|
|
18
42
|
/**
|
|
19
43
|
* Seeds the database from your models.
|
|
20
44
|
*
|
|
@@ -129,6 +153,11 @@ export declare interface SeederModel {
|
|
|
129
153
|
model: Model
|
|
130
154
|
filePath: string
|
|
131
155
|
}
|
|
156
|
+
/** A parent a model belongs to, and the column that points at it. */
|
|
157
|
+
export declare interface ParentRelation {
|
|
158
|
+
model: string
|
|
159
|
+
column: string
|
|
160
|
+
}
|
|
132
161
|
// Legacy exports for backwards compatibility
|
|
133
162
|
export { seed as runSeeders };
|
|
134
163
|
export { freshSeed as freshWithSeed };
|
package/dist/seeder.js
CHANGED
|
@@ -4,7 +4,7 @@ import { faker } from "@stacksjs/faker";
|
|
|
4
4
|
import { path } from "@stacksjs/path";
|
|
5
5
|
import { hashMake } from "@stacksjs/security";
|
|
6
6
|
import { fs } from "@stacksjs/storage";
|
|
7
|
-
function defaultModelsPath(subpath) {
|
|
7
|
+
export function defaultModelsPath(subpath) {
|
|
8
8
|
return path.frameworkPath(`defaults/app/Models/${subpath || ""}`);
|
|
9
9
|
}
|
|
10
10
|
export const PROTECTED_MODELS = Object.freeze([
|
|
@@ -153,18 +153,27 @@ async function existingRows(table) {
|
|
|
153
153
|
return [];
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
const modelTables = new Map;
|
|
157
|
+
export function registerModelTables(models) {
|
|
158
|
+
modelTables.clear();
|
|
159
|
+
for (const model of models)
|
|
160
|
+
modelTables.set(model.name, model.table);
|
|
161
|
+
}
|
|
162
|
+
export function parentTable(parent) {
|
|
163
|
+
return modelTables.get(parent) ?? `${snakeCase(parent)}s`;
|
|
164
|
+
}
|
|
156
165
|
async function relationColumns(model, options = {}) {
|
|
157
|
-
const parents =
|
|
166
|
+
const parents = parentRelations(model);
|
|
158
167
|
if (parents.length === 0)
|
|
159
168
|
return [];
|
|
160
169
|
const pools = [];
|
|
161
|
-
for (const
|
|
162
|
-
const column =
|
|
170
|
+
for (const relation of parents) {
|
|
171
|
+
const { model: parent, column } = relation;
|
|
163
172
|
if (model.attributes[parent])
|
|
164
173
|
continue;
|
|
165
174
|
if (isAccountModel(parent) && !options.allowProtected)
|
|
166
175
|
continue;
|
|
167
|
-
const rows = await existingRows(
|
|
176
|
+
const rows = await existingRows(parentTable(parent));
|
|
168
177
|
if (rows.length > 0)
|
|
169
178
|
pools.push({ column, rows });
|
|
170
179
|
}
|
|
@@ -273,13 +282,23 @@ async function seedModel(model, options) {
|
|
|
273
282
|
};
|
|
274
283
|
}
|
|
275
284
|
}
|
|
285
|
+
export function parentRelations(model) {
|
|
286
|
+
const belongsTo = model.model.belongsTo, read = (entry) => {
|
|
287
|
+
if (typeof entry === "string")
|
|
288
|
+
return entry ? { model: entry, column: `${snakeCase(entry)}_id` } : null;
|
|
289
|
+
if (entry && typeof entry === "object") {
|
|
290
|
+
const name = String(entry.model ?? "");
|
|
291
|
+
if (!name)
|
|
292
|
+
return null;
|
|
293
|
+
const key = entry.foreignKey;
|
|
294
|
+
return { model: name, column: key || `${snakeCase(name)}_id` };
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
};
|
|
298
|
+
return (Array.isArray(belongsTo) ? belongsTo : belongsTo && typeof belongsTo === "object" ? Object.values(belongsTo) : []).map(read).filter((relation) => relation !== null);
|
|
299
|
+
}
|
|
276
300
|
function parentModels(model) {
|
|
277
|
-
|
|
278
|
-
if (Array.isArray(belongsTo))
|
|
279
|
-
return belongsTo.map((entry) => typeof entry === "string" ? entry : String(entry?.model ?? "")).filter(Boolean);
|
|
280
|
-
if (belongsTo && typeof belongsTo === "object")
|
|
281
|
-
return Object.values(belongsTo).map((value) => String(value?.model ?? value)).filter(Boolean);
|
|
282
|
-
return [];
|
|
301
|
+
return parentRelations(model).map((relation) => relation.model);
|
|
283
302
|
}
|
|
284
303
|
async function clearTables(models, verbose) {
|
|
285
304
|
for (const model of [...models].reverse())
|
|
@@ -322,6 +341,7 @@ export async function seed(config = {}) {
|
|
|
322
341
|
log.info(`Default models directory: ${defaultModelsPath()}`);
|
|
323
342
|
}
|
|
324
343
|
let models = await loadAllModels(modelsDir, verbose, config.includeDefaults ?? !1);
|
|
344
|
+
registerModelTables(models);
|
|
325
345
|
if (models.length === 0) {
|
|
326
346
|
log.warn("No seedable models found in defaults or user directories");
|
|
327
347
|
return {
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createQueryBuilder, setConfig } from '@stacksjs/query-builder';
|
|
2
|
+
import type { QueryHooks } from '@stacksjs/query-builder';
|
|
2
3
|
export declare function acquireDbConfigLock(): Promise<() => void>;
|
|
3
4
|
// Function to initialize the config when it's available
|
|
4
5
|
export declare function initializeDbConfig(config: any): void;
|
|
6
|
+
export declare function createDatabaseQueryHooks(dispatch: (event: DatabaseQueryLogEvent) => void | Promise<void>): QueryHooks;
|
|
5
7
|
export declare function ensureDatabaseConfigLoaded(): Promise<void>;
|
|
6
8
|
declare function getDb(): ReturnType<typeof createQueryBuilder>;
|
|
7
9
|
/**
|
|
@@ -17,11 +19,33 @@ declare function getDb(): ReturnType<typeof createQueryBuilder>;
|
|
|
17
19
|
* different call sites, which is a sign the value wants one home.
|
|
18
20
|
*/
|
|
19
21
|
export declare const QB_SNAPSHOT_DIR: 'storage/framework/database';
|
|
22
|
+
/**
|
|
23
|
+
* Process-wide query-builder soft-delete filtering must stay disabled.
|
|
24
|
+
*
|
|
25
|
+
* The raw query builder has no model definition, so it cannot know whether
|
|
26
|
+
* the selected table carries the `useSoftDeletes` trait or even has a
|
|
27
|
+
* `deleted_at` column. ModelQueryBuilder applies the trait-aware scope, and
|
|
28
|
+
* the generated REST routes apply it from the model definition. Enabling
|
|
29
|
+
* this global filter would incorrectly scope every raw table query.
|
|
30
|
+
*/
|
|
31
|
+
export declare const RAW_QUERY_SOFT_DELETE_CONFIG: {
|
|
32
|
+
enabled: false;
|
|
33
|
+
column: 'deleted_at';
|
|
34
|
+
defaultFilter: true
|
|
35
|
+
};
|
|
20
36
|
/**
|
|
21
37
|
* Lazy proxy for the query builder - connection is only made when first used.
|
|
22
38
|
* This is the main entry point for database operations.
|
|
23
39
|
*/
|
|
24
40
|
export declare const db: Proxy;
|
|
41
|
+
export declare interface DatabaseQueryLogEvent {
|
|
42
|
+
query: {
|
|
43
|
+
sql: string
|
|
44
|
+
parameters?: unknown[]
|
|
45
|
+
}
|
|
46
|
+
queryDurationMillis: number
|
|
47
|
+
error?: unknown
|
|
48
|
+
}
|
|
25
49
|
/**
|
|
26
50
|
* Fluent chain returned by entry-point methods like `selectFrom`/`updateTable`.
|
|
27
51
|
*
|
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { createQueryBuilder, setConfig } from "@stacksjs/query-builder";
|
|
2
|
+
import { createQueryBuilder, registerPersistentQueryHooks, setConfig } from "@stacksjs/query-builder";
|
|
3
3
|
import { env as envVars } from "@stacksjs/env";
|
|
4
4
|
import { getConnectionDefaults } from "./defaults";
|
|
5
5
|
import { aggregateFunctions } from "./types";
|
|
@@ -87,7 +87,39 @@ function getDbConfig() {
|
|
|
87
87
|
}
|
|
88
88
|
return { database: ":memory:" };
|
|
89
89
|
}
|
|
90
|
-
export const QB_SNAPSHOT_DIR = "storage/framework/database"
|
|
90
|
+
export const QB_SNAPSHOT_DIR = "storage/framework/database", RAW_QUERY_SOFT_DELETE_CONFIG = {
|
|
91
|
+
enabled: !1,
|
|
92
|
+
column: "deleted_at",
|
|
93
|
+
defaultFilter: !0
|
|
94
|
+
};
|
|
95
|
+
export function createDatabaseQueryHooks(dispatch) {
|
|
96
|
+
function forward(event) {
|
|
97
|
+
try {
|
|
98
|
+
Promise.resolve(dispatch(event)).catch(() => {});
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
onQueryEnd: (event) => forward({
|
|
103
|
+
query: {
|
|
104
|
+
sql: event.sql,
|
|
105
|
+
parameters: event.params
|
|
106
|
+
},
|
|
107
|
+
queryDurationMillis: event.durationMs
|
|
108
|
+
}),
|
|
109
|
+
onQueryError: (event) => forward({
|
|
110
|
+
query: {
|
|
111
|
+
sql: event.sql,
|
|
112
|
+
parameters: event.params
|
|
113
|
+
},
|
|
114
|
+
queryDurationMillis: event.durationMs,
|
|
115
|
+
error: event.error
|
|
116
|
+
})
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function forwardDatabaseQuery(event) {
|
|
120
|
+
import("./query-logger").then(({ logQuery }) => logQuery(event)).catch(() => {});
|
|
121
|
+
}
|
|
122
|
+
registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));
|
|
91
123
|
function updateQueryBuilderConfig() {
|
|
92
124
|
const dialect = getDialect(), dbConfigForQb = getDbConfig();
|
|
93
125
|
setConfig({
|
|
@@ -100,11 +132,7 @@ function updateQueryBuilderConfig() {
|
|
|
100
132
|
updatedAt: "updated_at",
|
|
101
133
|
defaultOrderColumn: "created_at"
|
|
102
134
|
},
|
|
103
|
-
softDeletes:
|
|
104
|
-
enabled: !0,
|
|
105
|
-
column: "deleted_at",
|
|
106
|
-
defaultFilter: !0
|
|
107
|
-
}
|
|
135
|
+
softDeletes: RAW_QUERY_SOFT_DELETE_CONFIG
|
|
108
136
|
});
|
|
109
137
|
}
|
|
110
138
|
updateQueryBuilderConfig();
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/database",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.231",
|
|
6
6
|
"description": "The Stacks database integration.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -60,15 +60,15 @@
|
|
|
60
60
|
"dynamodb-tooling": "^0.3.2"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@stacksjs/cli": "0.70.
|
|
64
|
-
"@stacksjs/config": "0.70.
|
|
65
|
-
"@stacksjs/logging": "0.70.
|
|
66
|
-
"@stacksjs/router": "0.70.
|
|
63
|
+
"@stacksjs/cli": "0.70.231",
|
|
64
|
+
"@stacksjs/config": "0.70.231",
|
|
65
|
+
"@stacksjs/logging": "0.70.231",
|
|
66
|
+
"@stacksjs/router": "0.70.231",
|
|
67
67
|
"better-dx": "^0.2.17",
|
|
68
|
-
"@stacksjs/path": "0.70.
|
|
69
|
-
"@stacksjs/query-builder": "0.70.
|
|
70
|
-
"@stacksjs/storage": "0.70.
|
|
71
|
-
"@stacksjs/strings": "0.70.
|
|
72
|
-
"@stacksjs/utils": "0.70.
|
|
68
|
+
"@stacksjs/path": "0.70.231",
|
|
69
|
+
"@stacksjs/query-builder": "0.70.231",
|
|
70
|
+
"@stacksjs/storage": "0.70.231",
|
|
71
|
+
"@stacksjs/strings": "0.70.231",
|
|
72
|
+
"@stacksjs/utils": "0.70.231"
|
|
73
73
|
}
|
|
74
74
|
}
|