@stacksjs/database 0.70.256 → 0.70.258
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/auth-tables.js +12 -12
- package/dist/database.js +2 -1
- package/dist/datetime-columns.d.ts +36 -0
- package/dist/datetime-columns.js +85 -0
- package/dist/ddl-constraints.d.ts +31 -0
- package/dist/ddl-constraints.js +111 -0
- package/dist/dialect.d.ts +82 -0
- package/dist/dialect.js +79 -0
- package/dist/driver-config.d.ts +55 -5
- package/dist/driver-config.js +28 -0
- package/dist/drivers/defaults/index.d.ts +0 -1
- package/dist/drivers/defaults/index.js +0 -1
- package/dist/drivers/defaults/traits.d.ts +17 -31
- package/dist/drivers/defaults/traits.js +18 -1142
- package/dist/drivers/mysql.d.ts +0 -1
- package/dist/drivers/mysql.js +1 -14
- package/dist/drivers/postgres.d.ts +0 -1
- package/dist/drivers/postgres.js +2 -35
- package/dist/index.d.ts +20 -0
- package/dist/index.js +6 -0
- package/dist/notification-tables.js +6 -6
- package/dist/query-logger.js +2 -1
- package/dist/rbac-tables.d.ts +3 -3
- package/dist/rbac-tables.js +16 -13
- package/dist/replicas.d.ts +52 -0
- package/dist/replicas.js +74 -0
- package/dist/schema.d.ts +1 -0
- package/dist/sql-helpers.d.ts +60 -7
- package/dist/sql-helpers.js +31 -5
- package/dist/trait-tables.d.ts +126 -0
- package/dist/trait-tables.js +206 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.js +102 -13
- package/dist/vschema.d.ts +84 -0
- package/dist/vschema.js +121 -0
- package/package.json +11 -11
- package/dist/drivers/defaults/passwords.d.ts +0 -4
- package/dist/drivers/defaults/passwords.js +0 -106
package/dist/drivers/mysql.d.ts
CHANGED
|
@@ -2,6 +2,5 @@ import type { Ok } from '@stacksjs/error-handling';
|
|
|
2
2
|
export declare function resetMysqlDatabase(): Promise<Ok<string, never>>;
|
|
3
3
|
export declare function dropMysqlTables(): Promise<void>;
|
|
4
4
|
export declare function generateMysqlMigration(modelPath: string): Promise<void>;
|
|
5
|
-
export declare function generateMysqlTraitMigrations(): Promise<void>;
|
|
6
5
|
export declare function createAlterTableMigration(modelPath: string): Promise<void>;
|
|
7
6
|
export declare function generateIndexCreationSQL(tableName: string, index: { name: string, columns: string[], unique?: boolean, where?: string }): string;
|
package/dist/drivers/mysql.js
CHANGED
|
@@ -3,7 +3,6 @@ function italic(str) {
|
|
|
3
3
|
return `\x1B[3m${str}\x1B[23m`;
|
|
4
4
|
}
|
|
5
5
|
import { db } from "../utils";
|
|
6
|
-
import { createPasswordResetsTable } from "./defaults/passwords";
|
|
7
6
|
import { ok } from "@stacksjs/error-handling";
|
|
8
7
|
import { fetchOtherModelRelations, getModelName, getPivotTables, getTableName } from "@stacksjs/orm";
|
|
9
8
|
import { path } from "@stacksjs/path";
|
|
@@ -24,7 +23,7 @@ import {
|
|
|
24
23
|
mapFieldTypeToColumnType,
|
|
25
24
|
pluckChanges
|
|
26
25
|
} from "./helpers";
|
|
27
|
-
import {
|
|
26
|
+
import { dropCommonTables } from "./defaults/traits";
|
|
28
27
|
export async function resetMysqlDatabase() {
|
|
29
28
|
await dropMysqlTables();
|
|
30
29
|
await deleteFrameworkModels();
|
|
@@ -70,18 +69,6 @@ export async function generateMysqlMigration(modelPath) {
|
|
|
70
69
|
else
|
|
71
70
|
await createTableMigration(modelPath);
|
|
72
71
|
}
|
|
73
|
-
export async function generateMysqlTraitMigrations() {
|
|
74
|
-
await Promise.all([
|
|
75
|
-
createCategorizableTable(),
|
|
76
|
-
createCommentablesTable(),
|
|
77
|
-
createTaggableTable(),
|
|
78
|
-
createTaggablesTable(),
|
|
79
|
-
createPasswordResetsTable(),
|
|
80
|
-
createPasskeyMigration(),
|
|
81
|
-
createQueryLogsTable(),
|
|
82
|
-
createCommentUpvoteMigration()
|
|
83
|
-
]);
|
|
84
|
-
}
|
|
85
72
|
async function createTableMigration(modelPath) {
|
|
86
73
|
log.debug("createTableMigration modelPath:", modelPath);
|
|
87
74
|
const model = (await import(modelPath)).default, tableName = getTableName(model, modelPath), twoFactorEnabled = model.traits?.useAuth && typeof model.traits.useAuth !== "boolean" ? model.traits.useAuth.useTwoFactor : !1;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Ok } from '@stacksjs/error-handling';
|
|
2
2
|
export declare function dropPostgresTables(): Promise<void>;
|
|
3
|
-
export declare function generatePostgresTraitMigrations(): Promise<void>;
|
|
4
3
|
export declare function resetPostgresDatabase(): Promise<Ok<string, never>>;
|
|
5
4
|
export declare function generatePostgresMigration(modelPath: string): Promise<void>;
|
|
6
5
|
export declare function generateIndexCreationSQL(tableName: string, index: { name: string, columns: string[], unique?: boolean, where?: string }): string;
|
package/dist/drivers/postgres.js
CHANGED
|
@@ -3,7 +3,6 @@ function italic(str) {
|
|
|
3
3
|
return `\x1B[3m${str}\x1B[23m`;
|
|
4
4
|
}
|
|
5
5
|
import { db } from "../utils";
|
|
6
|
-
import { createPasswordResetsTable } from "./defaults/passwords";
|
|
7
6
|
import { ok } from "@stacksjs/error-handling";
|
|
8
7
|
import { fetchOtherModelRelations, getModelName, getPivotTables, getTableName } from "@stacksjs/orm";
|
|
9
8
|
import { path } from "@stacksjs/path";
|
|
@@ -23,51 +22,19 @@ import {
|
|
|
23
22
|
mapFieldTypeToColumnType,
|
|
24
23
|
pluckChanges
|
|
25
24
|
} from "./helpers";
|
|
26
|
-
import {
|
|
27
|
-
createPostgresCategorizableTable,
|
|
28
|
-
createPostgresCommentablesPivotTable,
|
|
29
|
-
createPostgresCommentsTable,
|
|
30
|
-
createPostgresCommentUpvoteMigration,
|
|
31
|
-
createPostgresPasskeyMigration,
|
|
32
|
-
createPostgresQueryLogsTable,
|
|
33
|
-
createPostgresTaggablesTable,
|
|
34
|
-
createPostgresTagsTable,
|
|
35
|
-
dropMigrationTables
|
|
36
|
-
} from "./defaults/traits";
|
|
25
|
+
import { dropCommonTables, dropMigrationTables } from "./defaults/traits";
|
|
37
26
|
export async function dropPostgresTables() {
|
|
38
27
|
const tables = await fetchPostgresTables(), userModelFiles = globSync([path.userModelsPath("*.ts"), path.storagePath("framework/defaults/app/Models/**/*.ts")], { absolute: !0 });
|
|
39
28
|
await dropMigrationTables();
|
|
40
29
|
for (const table of tables)
|
|
41
30
|
await db.unsafe(`DROP TABLE IF EXISTS "${table}" CASCADE`).execute();
|
|
42
|
-
await
|
|
31
|
+
await dropCommonTables();
|
|
43
32
|
for (const userModel of userModelFiles) {
|
|
44
33
|
const userModelPath = (await import(userModel)).default, pivotTables = await getPivotTables(userModelPath, userModel);
|
|
45
34
|
for (const pivotTable of pivotTables)
|
|
46
35
|
await db.unsafe(`DROP TABLE IF EXISTS "${pivotTable.table}" CASCADE`).execute();
|
|
47
36
|
}
|
|
48
37
|
}
|
|
49
|
-
async function dropCommonPostgresTables() {
|
|
50
|
-
await db.unsafe('DROP TABLE IF EXISTS "passkeys" CASCADE').execute();
|
|
51
|
-
await db.unsafe('DROP TABLE IF EXISTS "password_resets" CASCADE').execute();
|
|
52
|
-
await db.unsafe('DROP TABLE IF EXISTS "query_logs" CASCADE').execute();
|
|
53
|
-
await db.unsafe('DROP TABLE IF EXISTS "categorizables" CASCADE').execute();
|
|
54
|
-
await db.unsafe('DROP TABLE IF EXISTS "commentables" CASCADE').execute();
|
|
55
|
-
await db.unsafe('DROP TABLE IF EXISTS "comments" CASCADE').execute();
|
|
56
|
-
await db.unsafe('DROP TABLE IF EXISTS "tags" CASCADE').execute();
|
|
57
|
-
await db.unsafe('DROP TABLE IF EXISTS "taggables" CASCADE').execute();
|
|
58
|
-
await db.unsafe('DROP TABLE IF EXISTS "commentable_upvotes" CASCADE').execute();
|
|
59
|
-
}
|
|
60
|
-
export async function generatePostgresTraitMigrations() {
|
|
61
|
-
await createPostgresCategorizableTable();
|
|
62
|
-
await createPostgresCommentsTable();
|
|
63
|
-
await createPostgresTagsTable();
|
|
64
|
-
await createPostgresCommentUpvoteMigration();
|
|
65
|
-
await createPostgresPasskeyMigration();
|
|
66
|
-
await createPostgresQueryLogsTable();
|
|
67
|
-
await createPasswordResetsTable();
|
|
68
|
-
await createPostgresCommentablesPivotTable();
|
|
69
|
-
await createPostgresTaggablesTable();
|
|
70
|
-
}
|
|
71
38
|
export async function resetPostgresDatabase() {
|
|
72
39
|
await dropPostgresTables();
|
|
73
40
|
await deleteFrameworkModels();
|
package/dist/index.d.ts
CHANGED
|
@@ -101,6 +101,26 @@ export * from './relation-columns';
|
|
|
101
101
|
export { migrateNotificationTables } from './notification-tables';
|
|
102
102
|
// RBAC tables migration (stacksjs/stacks#1941 Phase A)
|
|
103
103
|
export { migrateRbacTables } from './rbac-tables';
|
|
104
|
+
// Polymorphic trait tables (commentables/taggables/categorizables/upvotes).
|
|
105
|
+
// Exported wholesale like auth-tables: the pure DDL builders are how tests and
|
|
106
|
+
// tooling stand up the same schema `buddy migrate` creates.
|
|
107
|
+
export * from './trait-tables';
|
|
108
|
+
// MySQL TIMESTAMP -> DATETIME guarantee for framework tables
|
|
109
|
+
export * from './datetime-columns';
|
|
110
|
+
// Dialect capability table — the single source of truth for what each
|
|
111
|
+
// dialect speaks (wire protocol) and what it accepts (DDL features).
|
|
112
|
+
export * from './dialect';
|
|
113
|
+
// Read replica routing policy (auto-route opt-in, transaction and
|
|
114
|
+
// read-your-writes carve-outs, replica selection).
|
|
115
|
+
export * from './replicas';
|
|
116
|
+
// DDL capability audit — catches a corpus that is valid SQL for the target's
|
|
117
|
+
// wire protocol but uses a feature the engine does not implement (foreign
|
|
118
|
+
// keys and AUTO_INCREMENT on a sharded engine).
|
|
119
|
+
export * from './ddl-constraints';
|
|
120
|
+
// VSchema derivation — turns the model relationship graph into a Vitess
|
|
121
|
+
// keyspace topology, co-locating child tables with their parents so joins
|
|
122
|
+
// between them do not scatter across shards.
|
|
123
|
+
export * from './vschema';
|
|
104
124
|
// SQL dialect helpers & connection defaults
|
|
105
125
|
export * from './sql-helpers';
|
|
106
126
|
export * from './defaults';
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,12 @@ export * from "./managed-columns";
|
|
|
27
27
|
export * from "./relation-columns";
|
|
28
28
|
export { migrateNotificationTables } from "./notification-tables";
|
|
29
29
|
export { migrateRbacTables } from "./rbac-tables";
|
|
30
|
+
export * from "./trait-tables";
|
|
31
|
+
export * from "./datetime-columns";
|
|
32
|
+
export * from "./dialect";
|
|
33
|
+
export * from "./replicas";
|
|
34
|
+
export * from "./ddl-constraints";
|
|
35
|
+
export * from "./vschema";
|
|
30
36
|
export * from "./sql-helpers";
|
|
31
37
|
export * from "./defaults";
|
|
32
38
|
export * from "./migration-dialect";
|
|
@@ -6,7 +6,7 @@ function getDbDriver() {
|
|
|
6
6
|
return process.env.DB_CONNECTION || envVars.DB_CONNECTION || "sqlite";
|
|
7
7
|
}
|
|
8
8
|
export function notificationsTableSql(sql) {
|
|
9
|
-
const { pkColumn, nullableTimestamp } = sql;
|
|
9
|
+
const { pkColumn, nullableTimestamp, datetime } = sql;
|
|
10
10
|
return `CREATE TABLE IF NOT EXISTS notifications (
|
|
11
11
|
${pkColumn},
|
|
12
12
|
user_id INTEGER NOT NULL,
|
|
@@ -14,25 +14,25 @@ export function notificationsTableSql(sql) {
|
|
|
14
14
|
data TEXT NOT NULL,
|
|
15
15
|
read_at ${nullableTimestamp},
|
|
16
16
|
uuid VARCHAR(36),
|
|
17
|
-
created_at
|
|
17
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
18
18
|
updated_at ${nullableTimestamp}
|
|
19
19
|
)`;
|
|
20
20
|
}
|
|
21
21
|
export function notificationPreferencesTableSql(sql) {
|
|
22
|
-
const { pkColumn, boolTrue, nullableTimestamp } = sql;
|
|
22
|
+
const { pkColumn, boolTrue, nullableTimestamp, datetime } = sql;
|
|
23
23
|
return `CREATE TABLE IF NOT EXISTS notification_preferences (
|
|
24
24
|
${pkColumn},
|
|
25
25
|
user_id INTEGER NOT NULL,
|
|
26
26
|
channel VARCHAR(50) NOT NULL,
|
|
27
27
|
enabled BOOLEAN NOT NULL DEFAULT ${boolTrue},
|
|
28
28
|
category VARCHAR(255),
|
|
29
|
-
created_at
|
|
29
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
30
30
|
updated_at ${nullableTimestamp},
|
|
31
31
|
UNIQUE (user_id, channel, category)
|
|
32
32
|
)`;
|
|
33
33
|
}
|
|
34
34
|
export function notificationDeliveriesTableSql(sql) {
|
|
35
|
-
const { pkColumn, nullableTimestamp } = sql;
|
|
35
|
+
const { pkColumn, nullableTimestamp, datetime } = sql;
|
|
36
36
|
return `CREATE TABLE IF NOT EXISTS notification_deliveries (
|
|
37
37
|
${pkColumn},
|
|
38
38
|
user_id INTEGER,
|
|
@@ -44,7 +44,7 @@ export function notificationDeliveriesTableSql(sql) {
|
|
|
44
44
|
error TEXT,
|
|
45
45
|
metadata TEXT,
|
|
46
46
|
sent_at ${nullableTimestamp},
|
|
47
|
-
created_at
|
|
47
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
48
48
|
updated_at ${nullableTimestamp}
|
|
49
49
|
)`;
|
|
50
50
|
}
|
package/dist/query-logger.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sqlDateTime } from "./sql-helpers";
|
|
1
2
|
import { memoryUsage } from "node:process";
|
|
2
3
|
import { config } from "@stacksjs/config";
|
|
3
4
|
import { log } from "@stacksjs/logging";
|
|
@@ -89,7 +90,7 @@ async function createQueryLogRecord(query, durationMs, status, error, bindings)
|
|
|
89
90
|
connection,
|
|
90
91
|
status,
|
|
91
92
|
error: error ? String(error) : void 0,
|
|
92
|
-
executed_at:
|
|
93
|
+
executed_at: sqlDateTime(),
|
|
93
94
|
bindings,
|
|
94
95
|
trace,
|
|
95
96
|
...caller,
|
package/dist/rbac-tables.d.ts
CHANGED
|
@@ -4,11 +4,11 @@ export declare function rolesTableSql(sql: SqlHelpers): string;
|
|
|
4
4
|
/** `permissions` table — same shape as `roles`. */
|
|
5
5
|
export declare function permissionsTableSql(sql: SqlHelpers): string;
|
|
6
6
|
/** `user_roles` pivot — composite PK makes double-assign a unique violation. */
|
|
7
|
-
export declare function userRolesTableSql(): string;
|
|
7
|
+
export declare function userRolesTableSql(sql: SqlHelpers): string;
|
|
8
8
|
/** `user_permissions` pivot. */
|
|
9
|
-
export declare function userPermissionsTableSql(): string;
|
|
9
|
+
export declare function userPermissionsTableSql(sql: SqlHelpers): string;
|
|
10
10
|
/** `role_permissions` pivot. */
|
|
11
|
-
export declare function rolePermissionsTableSql(): string;
|
|
11
|
+
export declare function rolePermissionsTableSql(sql: SqlHelpers): string;
|
|
12
12
|
/**
|
|
13
13
|
* Create the 5 RBAC tables. Idempotent (`IF NOT EXISTS`), so it's
|
|
14
14
|
* safe to run on every `buddy migrate`.
|
package/dist/rbac-tables.js
CHANGED
|
@@ -6,50 +6,53 @@ function getDbDriver() {
|
|
|
6
6
|
return envVars.DB_CONNECTION || "sqlite";
|
|
7
7
|
}
|
|
8
8
|
export function rolesTableSql(sql) {
|
|
9
|
-
const { pkColumn, nullableTimestamp } = sql;
|
|
9
|
+
const { pkColumn, nullableTimestamp, datetime } = sql;
|
|
10
10
|
return `CREATE TABLE IF NOT EXISTS roles (
|
|
11
11
|
${pkColumn},
|
|
12
12
|
name VARCHAR(255) NOT NULL,
|
|
13
13
|
guard_name VARCHAR(255) NOT NULL DEFAULT 'web',
|
|
14
14
|
description TEXT,
|
|
15
|
-
created_at
|
|
15
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
16
16
|
updated_at ${nullableTimestamp},
|
|
17
17
|
UNIQUE (name, guard_name)
|
|
18
18
|
)`;
|
|
19
19
|
}
|
|
20
20
|
export function permissionsTableSql(sql) {
|
|
21
|
-
const { pkColumn, nullableTimestamp } = sql;
|
|
21
|
+
const { pkColumn, nullableTimestamp, datetime } = sql;
|
|
22
22
|
return `CREATE TABLE IF NOT EXISTS permissions (
|
|
23
23
|
${pkColumn},
|
|
24
24
|
name VARCHAR(255) NOT NULL,
|
|
25
25
|
guard_name VARCHAR(255) NOT NULL DEFAULT 'web',
|
|
26
26
|
description TEXT,
|
|
27
|
-
created_at
|
|
27
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
28
28
|
updated_at ${nullableTimestamp},
|
|
29
29
|
UNIQUE (name, guard_name)
|
|
30
30
|
)`;
|
|
31
31
|
}
|
|
32
|
-
export function userRolesTableSql() {
|
|
32
|
+
export function userRolesTableSql(sql) {
|
|
33
|
+
const { datetime } = sql;
|
|
33
34
|
return `CREATE TABLE IF NOT EXISTS user_roles (
|
|
34
35
|
user_id INTEGER NOT NULL,
|
|
35
36
|
role_id INTEGER NOT NULL,
|
|
36
|
-
created_at
|
|
37
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
37
38
|
PRIMARY KEY (user_id, role_id)
|
|
38
39
|
)`;
|
|
39
40
|
}
|
|
40
|
-
export function userPermissionsTableSql() {
|
|
41
|
+
export function userPermissionsTableSql(sql) {
|
|
42
|
+
const { datetime } = sql;
|
|
41
43
|
return `CREATE TABLE IF NOT EXISTS user_permissions (
|
|
42
44
|
user_id INTEGER NOT NULL,
|
|
43
45
|
permission_id INTEGER NOT NULL,
|
|
44
|
-
created_at
|
|
46
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
45
47
|
PRIMARY KEY (user_id, permission_id)
|
|
46
48
|
)`;
|
|
47
49
|
}
|
|
48
|
-
export function rolePermissionsTableSql() {
|
|
50
|
+
export function rolePermissionsTableSql(sql) {
|
|
51
|
+
const { datetime } = sql;
|
|
49
52
|
return `CREATE TABLE IF NOT EXISTS role_permissions (
|
|
50
53
|
role_id INTEGER NOT NULL,
|
|
51
54
|
permission_id INTEGER NOT NULL,
|
|
52
|
-
created_at
|
|
55
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
53
56
|
PRIMARY KEY (role_id, permission_id)
|
|
54
57
|
)`;
|
|
55
58
|
}
|
|
@@ -66,13 +69,13 @@ export async function migrateRbacTables(options = {}) {
|
|
|
66
69
|
await db.unsafe(permissionsTableSql(sql)).execute();
|
|
67
70
|
if (options.verbose)
|
|
68
71
|
log.info("Creating user_roles pivot...");
|
|
69
|
-
await db.unsafe(userRolesTableSql()).execute();
|
|
72
|
+
await db.unsafe(userRolesTableSql(sql)).execute();
|
|
70
73
|
if (options.verbose)
|
|
71
74
|
log.info("Creating user_permissions pivot...");
|
|
72
|
-
await db.unsafe(userPermissionsTableSql()).execute();
|
|
75
|
+
await db.unsafe(userPermissionsTableSql(sql)).execute();
|
|
73
76
|
if (options.verbose)
|
|
74
77
|
log.info("Creating role_permissions pivot...");
|
|
75
|
-
await db.unsafe(rolePermissionsTableSql()).execute();
|
|
78
|
+
await db.unsafe(rolePermissionsTableSql(sql)).execute();
|
|
76
79
|
if (options.verbose)
|
|
77
80
|
log.success("RBAC tables created");
|
|
78
81
|
return { success: !0 };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReadPolicyConfig, ReplicaConfig } from './driver-config';
|
|
2
|
+
/**
|
|
3
|
+
* Run `fn` in a fresh routing context.
|
|
4
|
+
*
|
|
5
|
+
* Called per request by the HTTP layer so read-your-writes is scoped to
|
|
6
|
+
* one request. Without an enclosing context the router falls back to
|
|
7
|
+
* treating every read as routable, which is the correct behavior for
|
|
8
|
+
* background work that has no request boundary to speak of — such work
|
|
9
|
+
* should use `db.read` explicitly if it wants a replica.
|
|
10
|
+
*/
|
|
11
|
+
export declare function withRoutingContext<T>(fn: () => T): T;
|
|
12
|
+
/** Record that the current context has written, pinning its later reads. */
|
|
13
|
+
export declare function markContextWrote(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Mark the current context as inside a transaction for the duration of
|
|
16
|
+
* `fn`, restoring the previous value afterwards so nested transactions
|
|
17
|
+
* unwind correctly.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withTransactionContext<T>(fn: () => Promise<T>): Promise<T>;
|
|
20
|
+
/** Whether the current async context has already issued a write. */
|
|
21
|
+
export declare function contextHasWritten(): boolean;
|
|
22
|
+
/** Whether the current async context is inside a transaction. */
|
|
23
|
+
export declare function contextInTransaction(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether a read issued right now may go to a replica.
|
|
26
|
+
*
|
|
27
|
+
* The single decision point for rules 1-3. Exported so the routing
|
|
28
|
+
* behavior can be asserted directly in tests without standing up two
|
|
29
|
+
* database servers.
|
|
30
|
+
*/
|
|
31
|
+
export declare function shouldRouteToReplica(options: {
|
|
32
|
+
policy?: ReadPolicyConfig
|
|
33
|
+
replicas?: ReplicaConfig[]
|
|
34
|
+
}): boolean;
|
|
35
|
+
/** Reset the cursor. Test-only — keeps selection assertions deterministic. */
|
|
36
|
+
export declare function resetReplicaCursor(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Pick a replica according to the configured strategy.
|
|
39
|
+
*
|
|
40
|
+
* Returns `undefined` for an empty list so callers fall back to the
|
|
41
|
+
* primary rather than having to pre-check.
|
|
42
|
+
*/
|
|
43
|
+
export declare function selectReplica(replicas: ReplicaConfig[], strategy?: ReadPolicyConfig['strategy']): ReplicaConfig | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a replica's connection settings against its primary.
|
|
46
|
+
*
|
|
47
|
+
* A replica declares only what differs — usually just the host — so
|
|
48
|
+
* everything else is inherited. Getting this wrong in the other direction
|
|
49
|
+
* (requiring full credentials per replica) is how host lists drift out of
|
|
50
|
+
* sync with a rotated password.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveReplicaConnection(replica: ReplicaConfig, primary: { name?: string, database?: string, host?: string, port?: number, username?: string, password?: string }): { database: string, host: string, port?: number, username?: string, password?: string };
|
package/dist/replicas.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const routingContext = new AsyncLocalStorage;
|
|
3
|
+
export function withRoutingContext(fn) {
|
|
4
|
+
return routingContext.run({ wroteInContext: !1, inTransaction: !1 }, fn);
|
|
5
|
+
}
|
|
6
|
+
export function markContextWrote() {
|
|
7
|
+
const store = routingContext.getStore();
|
|
8
|
+
if (store)
|
|
9
|
+
store.wroteInContext = !0;
|
|
10
|
+
}
|
|
11
|
+
export async function withTransactionContext(fn) {
|
|
12
|
+
const store = routingContext.getStore();
|
|
13
|
+
if (!store)
|
|
14
|
+
return fn();
|
|
15
|
+
const previous = store.inTransaction;
|
|
16
|
+
store.inTransaction = !0;
|
|
17
|
+
try {
|
|
18
|
+
return await fn();
|
|
19
|
+
} finally {
|
|
20
|
+
store.inTransaction = previous;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function contextHasWritten() {
|
|
24
|
+
return routingContext.getStore()?.wroteInContext ?? !1;
|
|
25
|
+
}
|
|
26
|
+
export function contextInTransaction() {
|
|
27
|
+
return routingContext.getStore()?.inTransaction ?? !1;
|
|
28
|
+
}
|
|
29
|
+
export function shouldRouteToReplica(options) {
|
|
30
|
+
const { policy, replicas } = options;
|
|
31
|
+
if (!replicas?.length)
|
|
32
|
+
return !1;
|
|
33
|
+
if (!policy?.autoRoute)
|
|
34
|
+
return !1;
|
|
35
|
+
if (contextInTransaction())
|
|
36
|
+
return !1;
|
|
37
|
+
if (contextHasWritten())
|
|
38
|
+
return !1;
|
|
39
|
+
return !0;
|
|
40
|
+
}
|
|
41
|
+
let roundRobinCursor = 0;
|
|
42
|
+
export function resetReplicaCursor() {
|
|
43
|
+
roundRobinCursor = 0;
|
|
44
|
+
}
|
|
45
|
+
export function selectReplica(replicas, strategy = "round-robin", random = Math.random) {
|
|
46
|
+
if (!replicas.length)
|
|
47
|
+
return;
|
|
48
|
+
if (replicas.length === 1)
|
|
49
|
+
return replicas[0];
|
|
50
|
+
if (strategy === "random")
|
|
51
|
+
return replicas[Math.floor(random() * replicas.length)];
|
|
52
|
+
if (strategy === "weighted") {
|
|
53
|
+
const weights = replicas.map((r) => Math.max(0, r.weight ?? 1)), total = weights.reduce((sum, w) => sum + w, 0);
|
|
54
|
+
if (total <= 0)
|
|
55
|
+
return replicas[roundRobinCursor++ % replicas.length];
|
|
56
|
+
let ticket = random() * total;
|
|
57
|
+
for (let i = 0;i < replicas.length; i++) {
|
|
58
|
+
ticket -= weights[i];
|
|
59
|
+
if (ticket < 0)
|
|
60
|
+
return replicas[i];
|
|
61
|
+
}
|
|
62
|
+
return replicas[replicas.length - 1];
|
|
63
|
+
}
|
|
64
|
+
return replicas[roundRobinCursor++ % replicas.length];
|
|
65
|
+
}
|
|
66
|
+
export function resolveReplicaConnection(replica, primary) {
|
|
67
|
+
return {
|
|
68
|
+
database: primary.name ?? primary.database ?? "",
|
|
69
|
+
host: replica.host,
|
|
70
|
+
port: replica.port ?? primary.port,
|
|
71
|
+
username: replica.username ?? primary.username,
|
|
72
|
+
password: replica.password ?? primary.password
|
|
73
|
+
};
|
|
74
|
+
}
|
package/dist/schema.d.ts
CHANGED
package/dist/sql-helpers.d.ts
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework's canonical datetime literal: ISO-8601 UTC **without** the
|
|
3
|
+
* trailing `Z` (`2026-08-04T01:52:47.417`).
|
|
4
|
+
*
|
|
5
|
+
* One format for every dialect, because the alternatives are all broken:
|
|
6
|
+
*
|
|
7
|
+
* - `new Date().toISOString()` — what the framework used to write — is
|
|
8
|
+
* rejected outright by MySQL in strict mode ("Incorrect datetime value"),
|
|
9
|
+
* so any insert into a TIMESTAMP column threw. Dropping the `Z` is the
|
|
10
|
+
* whole difference; MySQL accepts the rest of the ISO shape.
|
|
11
|
+
* - A space-separated literal (`2026-08-04 01:52:47`) is accepted everywhere
|
|
12
|
+
* but sorts BEFORE an ISO string on SQLite, where these columns hold text.
|
|
13
|
+
* Mixing it with already-stored ISO rows would corrupt every ordering and
|
|
14
|
+
* range query over existing data.
|
|
15
|
+
*
|
|
16
|
+
* Keeping the `T` and dropping only the `Z` satisfies all three engines and
|
|
17
|
+
* still sorts correctly against rows written in the old format, since the
|
|
18
|
+
* value is a prefix of the old one for the same instant.
|
|
19
|
+
*
|
|
20
|
+
* Use this for every app-generated timestamp written to, or compared against,
|
|
21
|
+
* a framework table — never the raw `toISOString()`, and never the database's
|
|
22
|
+
* own clock (see below).
|
|
23
|
+
*
|
|
24
|
+
* ## Do not compare these against `NOW()` / `datetime('now')`
|
|
25
|
+
*
|
|
26
|
+
* The DB clock renders as `2026-08-04 01:52:47` — space-separated. On SQLite
|
|
27
|
+
* these columns are text, and `'T' > ' '`, so an ISO value always compares
|
|
28
|
+
* greater than a same-day DB-clock value regardless of the actual instant.
|
|
29
|
+
* That silently made every `expires_at > datetime('now')` check pass for
|
|
30
|
+
* already-expired rows. Compare app-written columns against `sqlDateTime()`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function sqlDateTime(value?: Date): string;
|
|
33
|
+
/**
|
|
34
|
+
* {@link sqlDateTime} pre-quoted for interpolation into a raw SQL string.
|
|
35
|
+
* The value is generated from a `Date`, never from user input, so it cannot
|
|
36
|
+
* carry a quote to escape.
|
|
37
|
+
*/
|
|
38
|
+
export declare function sqlDateTimeLiteral(value?: Date): string;
|
|
39
|
+
/**
|
|
40
|
+
* Read a timestamp back out of a framework table.
|
|
41
|
+
*
|
|
42
|
+
* The counterpart to {@link sqlDateTime}, and mandatory wherever a stored
|
|
43
|
+
* timestamp is compared in JavaScript. `new Date('2026-08-04T01:52:47.417')`
|
|
44
|
+
* — an ISO date-time with no offset — is parsed as **local** time per the ES
|
|
45
|
+
* spec, while the same string with a `Z` is UTC. Since the stored format
|
|
46
|
+
* cannot carry the `Z` (MySQL rejects it), a bare `new Date(...)` on these
|
|
47
|
+
* values silently shifts every comparison by the host's UTC offset. Only a
|
|
48
|
+
* server already running in UTC would look correct.
|
|
49
|
+
*
|
|
50
|
+
* Accepts every shape these columns hold: the canonical `T` format, the
|
|
51
|
+
* space-separated form the database clocks emit, values that still carry a
|
|
52
|
+
* `Z` or an explicit offset from before this format existed, and the `Date`
|
|
53
|
+
* objects the MySQL driver hands back. Anything without an explicit offset is
|
|
54
|
+
* read as UTC, which is what the framework writes.
|
|
55
|
+
*
|
|
56
|
+
* Returns null for missing or unparseable input so callers can fail closed
|
|
57
|
+
* rather than treating a bad value as the epoch.
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseSqlDateTime(value: unknown): Date | null;
|
|
1
60
|
/**
|
|
2
61
|
* Create SQL dialect helpers for a given driver.
|
|
3
62
|
*
|
|
@@ -9,13 +68,6 @@
|
|
|
9
68
|
* ```
|
|
10
69
|
*/
|
|
11
70
|
export declare function sqlHelpers(driver: string): SqlDialectHelpers;
|
|
12
|
-
/**
|
|
13
|
-
* SQL Dialect Helpers
|
|
14
|
-
*
|
|
15
|
-
* Cross-database compatibility utilities for PostgreSQL, MySQL, and SQLite.
|
|
16
|
-
* Centralizes the isPostgres/isMysql/now/boolTrue/boolFalse/param helpers
|
|
17
|
-
* that were previously duplicated across tokens.ts, auth-tables.ts, and setup.ts.
|
|
18
|
-
*/
|
|
19
71
|
export declare interface SqlDialectHelpers {
|
|
20
72
|
driver: string
|
|
21
73
|
isPostgres: boolean
|
|
@@ -27,6 +79,7 @@ export declare interface SqlDialectHelpers {
|
|
|
27
79
|
autoIncrement: string
|
|
28
80
|
primaryKey: string
|
|
29
81
|
pkColumn: string
|
|
82
|
+
datetime: string
|
|
30
83
|
nullableTimestamp: string
|
|
31
84
|
param: (index: number) => string
|
|
32
85
|
params: (...values: unknown[]) => { sql: string, values: unknown[] }
|
package/dist/sql-helpers.js
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
|
+
import { dialectCapabilities } from "./dialect";
|
|
2
|
+
export function sqlDateTime(value = new Date) {
|
|
3
|
+
return value.toISOString().slice(0, -1);
|
|
4
|
+
}
|
|
5
|
+
export function sqlDateTimeLiteral(value = new Date) {
|
|
6
|
+
return `'${sqlDateTime(value)}'`;
|
|
7
|
+
}
|
|
8
|
+
export function parseSqlDateTime(value) {
|
|
9
|
+
if (value === null || value === void 0)
|
|
10
|
+
return null;
|
|
11
|
+
if (value instanceof Date)
|
|
12
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
13
|
+
if (typeof value === "number")
|
|
14
|
+
return Number.isNaN(value) ? null : new Date(value);
|
|
15
|
+
if (typeof value !== "string")
|
|
16
|
+
return null;
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return null;
|
|
20
|
+
let normalized = trimmed.replace(" ", "T");
|
|
21
|
+
if (!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(normalized))
|
|
22
|
+
normalized += "Z";
|
|
23
|
+
const parsed = new Date(normalized);
|
|
24
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
25
|
+
}
|
|
1
26
|
export function sqlHelpers(driver) {
|
|
2
|
-
const
|
|
27
|
+
const caps = dialectCapabilities(driver), isPostgres = caps.wire === "postgres", isMysql = caps.wire === "mysql", isSqlite = caps.wire === "sqlite";
|
|
3
28
|
return {
|
|
4
29
|
driver,
|
|
5
30
|
isPostgres,
|
|
6
31
|
isMysql,
|
|
7
|
-
isSqlite
|
|
32
|
+
isSqlite,
|
|
8
33
|
now: isPostgres || isMysql ? "NOW()" : "datetime('now')",
|
|
9
34
|
boolTrue: isPostgres ? "true" : "1",
|
|
10
35
|
boolFalse: isPostgres ? "false" : "0",
|
|
11
36
|
autoIncrement: isPostgres ? "SERIAL" : "INTEGER",
|
|
12
|
-
primaryKey: isPostgres ? "PRIMARY KEY" : isMysql ? "PRIMARY KEY AUTO_INCREMENT" : "PRIMARY KEY AUTOINCREMENT",
|
|
13
|
-
pkColumn: isPostgres ? "id SERIAL PRIMARY KEY" : isMysql ? "id INTEGER PRIMARY KEY AUTO_INCREMENT" : "id INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
14
|
-
|
|
37
|
+
primaryKey: !caps.supportsAutoIncrement ? "PRIMARY KEY" : isPostgres ? "PRIMARY KEY" : isMysql ? "PRIMARY KEY AUTO_INCREMENT" : "PRIMARY KEY AUTOINCREMENT",
|
|
38
|
+
pkColumn: !caps.supportsAutoIncrement ? "id BIGINT NOT NULL PRIMARY KEY" : isPostgres ? "id SERIAL PRIMARY KEY" : isMysql ? "id INTEGER PRIMARY KEY AUTO_INCREMENT" : "id INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
39
|
+
datetime: isMysql ? "DATETIME" : "TIMESTAMP",
|
|
40
|
+
nullableTimestamp: isMysql ? "DATETIME NULL" : "TIMESTAMP",
|
|
15
41
|
param(index) {
|
|
16
42
|
return isPostgres ? `$${index}` : "?";
|
|
17
43
|
},
|