@stacksjs/database 0.70.228 → 0.70.230

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.
@@ -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 = parentModels(model);
166
+ const parents = parentRelations(model);
158
167
  if (parents.length === 0)
159
168
  return [];
160
169
  const pools = [];
161
- for (const parent of parents) {
162
- const column = `${snakeCase(parent)}_id`;
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(`${snakeCase(parent)}s`);
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
- const belongsTo = model.model.belongsTo;
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.228",
5
+ "version": "0.70.230",
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.228",
64
- "@stacksjs/config": "0.70.228",
65
- "@stacksjs/logging": "0.70.228",
66
- "@stacksjs/router": "0.70.228",
63
+ "@stacksjs/cli": "0.70.230",
64
+ "@stacksjs/config": "0.70.230",
65
+ "@stacksjs/logging": "0.70.230",
66
+ "@stacksjs/router": "0.70.230",
67
67
  "better-dx": "^0.2.17",
68
- "@stacksjs/path": "0.70.228",
69
- "@stacksjs/query-builder": "0.70.228",
70
- "@stacksjs/storage": "0.70.228",
71
- "@stacksjs/strings": "0.70.228",
72
- "@stacksjs/utils": "0.70.228"
68
+ "@stacksjs/path": "0.70.230",
69
+ "@stacksjs/query-builder": "0.70.230",
70
+ "@stacksjs/storage": "0.70.230",
71
+ "@stacksjs/strings": "0.70.230",
72
+ "@stacksjs/utils": "0.70.230"
73
73
  }
74
74
  }