@stacksjs/database 0.70.257 → 0.70.259
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 +18 -137
- package/dist/column.js +1 -26
- package/dist/custom/audits.js +20 -54
- package/dist/custom/errors.js +16 -46
- package/dist/custom/index.js +1 -3
- package/dist/custom/jobs.js +13 -137
- package/dist/database.js +1 -181
- package/dist/datetime-columns.js +8 -85
- package/dist/ddl-constraints.js +7 -111
- package/dist/defaults.js +1 -48
- package/dist/dialect.js +1 -79
- package/dist/driver-config.js +1 -172
- package/dist/drivers/defaults/index.js +1 -1
- package/dist/drivers/defaults/traits.js +1 -29
- package/dist/drivers/dynamodb.js +1 -607
- package/dist/drivers/helpers.js +1 -206
- package/dist/drivers/index.js +1 -9
- package/dist/drivers/mysql.js +58 -299
- package/dist/drivers/postgres.js +78 -368
- package/dist/drivers/sqlite.js +61 -379
- package/dist/ensure-database.js +1 -145
- package/dist/fk-audit.js +3 -187
- package/dist/index.js +1 -64
- package/dist/managed-columns.js +1 -59
- package/dist/migration-dialect.js +4 -107
- package/dist/migration-ledger.js +1 -382
- package/dist/migration-lock.js +1 -143
- package/dist/migrations.js +15 -1118
- package/dist/model-sources.js +1 -76
- package/dist/notification-tables.js +4 -49
- package/dist/query-logger.js +2 -241
- package/dist/query-parser.js +1 -93
- package/dist/rbac-tables.js +6 -61
- package/dist/relation-columns.js +1 -66
- package/dist/replicas.js +1 -74
- package/dist/safe-migrations.js +2 -52
- package/dist/schema.js +1 -10
- package/dist/seeder.js +1 -457
- package/dist/sql-helpers.js +1 -50
- package/dist/table.js +1 -26
- package/dist/tools/setup.js +1 -6
- package/dist/trait-tables.js +8 -153
- package/dist/transaction-context.js +1 -62
- package/dist/types.js +1 -98
- package/dist/unique-audit.js +3 -155
- package/dist/utils.js +1 -285
- package/dist/uuid-columns.js +1 -68
- package/dist/validators.js +1 -122
- package/dist/vschema.js +2 -121
- package/package.json +20 -13
package/dist/trait-tables.js
CHANGED
|
@@ -1,31 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { env as envVars } from "@stacksjs/env";
|
|
3
|
-
import { db } from "./utils";
|
|
4
|
-
import { sqlHelpers } from "./sql-helpers";
|
|
5
|
-
import { dialectCapabilities } from "./dialect";
|
|
6
|
-
function getDbDriver() {
|
|
7
|
-
return process.env.DB_CONNECTION || envVars.DB_CONNECTION || "sqlite";
|
|
8
|
-
}
|
|
9
|
-
function createdAt(sql) {
|
|
10
|
-
return `created_at ${sql.datetime}`;
|
|
11
|
-
}
|
|
12
|
-
function updatedAt(sql) {
|
|
13
|
-
return `updated_at ${sql.nullableTimestamp}`;
|
|
14
|
-
}
|
|
15
|
-
export function traitTableNames() {
|
|
16
|
-
return [
|
|
17
|
-
"commentables",
|
|
18
|
-
"taggables",
|
|
19
|
-
"categorizables",
|
|
20
|
-
"commentable_upvotes",
|
|
21
|
-
"taggable_models",
|
|
22
|
-
"categorizable_models"
|
|
23
|
-
];
|
|
24
|
-
}
|
|
25
|
-
export const UNSCOPED_OWNER_ID = 0;
|
|
26
|
-
export function commentablesTableSql(sql) {
|
|
27
|
-
const { pkColumn, boolTrue } = sql;
|
|
28
|
-
return `CREATE TABLE IF NOT EXISTS commentables (
|
|
1
|
+
import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{dialectCapabilities}from"./dialect";function getDbDriver(){return process.env.DB_CONNECTION||envVars.DB_CONNECTION||"sqlite"}function createdAt(sql){return`created_at ${sql.datetime}`}function updatedAt(sql){return`updated_at ${sql.nullableTimestamp}`}export function traitTableNames(){return["commentables","taggables","categorizables","commentable_upvotes","taggable_models","categorizable_models"]}export const UNSCOPED_OWNER_ID=0;export function commentablesTableSql(sql){const{pkColumn,boolTrue}=sql;return`CREATE TABLE IF NOT EXISTS commentables (
|
|
29
2
|
${pkColumn},
|
|
30
3
|
title VARCHAR(255) NOT NULL,
|
|
31
4
|
body TEXT NOT NULL,
|
|
@@ -38,11 +11,7 @@ export function commentablesTableSql(sql) {
|
|
|
38
11
|
is_active BOOLEAN NOT NULL DEFAULT ${boolTrue},
|
|
39
12
|
${createdAt(sql)},
|
|
40
13
|
${updatedAt(sql)}
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
export function taggablesTableSql(sql) {
|
|
44
|
-
const { pkColumn, boolTrue } = sql;
|
|
45
|
-
return `CREATE TABLE IF NOT EXISTS taggables (
|
|
14
|
+
)`}export function taggablesTableSql(sql){const{pkColumn,boolTrue}=sql;return`CREATE TABLE IF NOT EXISTS taggables (
|
|
46
15
|
${pkColumn},
|
|
47
16
|
name VARCHAR(255) NOT NULL,
|
|
48
17
|
slug VARCHAR(255) NOT NULL,
|
|
@@ -52,11 +21,7 @@ export function taggablesTableSql(sql) {
|
|
|
52
21
|
taggable_type VARCHAR(255) NOT NULL,
|
|
53
22
|
${createdAt(sql)},
|
|
54
23
|
${updatedAt(sql)}
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
export function categorizablesTableSql(sql) {
|
|
58
|
-
const { pkColumn, boolTrue } = sql;
|
|
59
|
-
return `CREATE TABLE IF NOT EXISTS categorizables (
|
|
24
|
+
)`}export function categorizablesTableSql(sql){const{pkColumn,boolTrue}=sql;return`CREATE TABLE IF NOT EXISTS categorizables (
|
|
60
25
|
${pkColumn},
|
|
61
26
|
name VARCHAR(255) NOT NULL,
|
|
62
27
|
slug VARCHAR(255) NOT NULL,
|
|
@@ -66,141 +31,31 @@ export function categorizablesTableSql(sql) {
|
|
|
66
31
|
categorizable_type VARCHAR(255) NOT NULL,
|
|
67
32
|
${createdAt(sql)},
|
|
68
33
|
${updatedAt(sql)}
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
export function taggableModelsTableSql(sql) {
|
|
72
|
-
const { pkColumn } = sql;
|
|
73
|
-
return `CREATE TABLE IF NOT EXISTS taggable_models (
|
|
34
|
+
)`}export function taggableModelsTableSql(sql){const{pkColumn}=sql;return`CREATE TABLE IF NOT EXISTS taggable_models (
|
|
74
35
|
${pkColumn},
|
|
75
36
|
tag_id INTEGER NOT NULL,
|
|
76
37
|
taggable_id INTEGER NOT NULL,
|
|
77
38
|
taggable_type VARCHAR(255) NOT NULL,
|
|
78
39
|
${createdAt(sql)},
|
|
79
40
|
${updatedAt(sql)}
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
export function categorizableModelsTableSql(sql) {
|
|
83
|
-
const { pkColumn } = sql;
|
|
84
|
-
return `CREATE TABLE IF NOT EXISTS categorizable_models (
|
|
41
|
+
)`}export function categorizableModelsTableSql(sql){const{pkColumn}=sql;return`CREATE TABLE IF NOT EXISTS categorizable_models (
|
|
85
42
|
${pkColumn},
|
|
86
43
|
category_id INTEGER NOT NULL,
|
|
87
44
|
categorizable_id INTEGER NOT NULL,
|
|
88
45
|
categorizable_type VARCHAR(255) NOT NULL,
|
|
89
46
|
${createdAt(sql)},
|
|
90
47
|
${updatedAt(sql)}
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
export function likesTableSql(sql, table, foreignKey) {
|
|
94
|
-
const { pkColumn } = sql;
|
|
95
|
-
return `CREATE TABLE IF NOT EXISTS ${table} (
|
|
48
|
+
)`}export function likesTableSql(sql,table,foreignKey){const{pkColumn}=sql;return`CREATE TABLE IF NOT EXISTS ${table} (
|
|
96
49
|
${pkColumn},
|
|
97
50
|
${foreignKey} INTEGER NOT NULL,
|
|
98
51
|
user_id INTEGER NOT NULL,
|
|
99
52
|
${createdAt(sql)},
|
|
100
53
|
${updatedAt(sql)},
|
|
101
54
|
UNIQUE (${foreignKey}, user_id)
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
export function commentableUpvotesTableSql(sql) {
|
|
105
|
-
const { pkColumn } = sql;
|
|
106
|
-
return `CREATE TABLE IF NOT EXISTS commentable_upvotes (
|
|
55
|
+
)`}export function commentableUpvotesTableSql(sql){const{pkColumn}=sql;return`CREATE TABLE IF NOT EXISTS commentable_upvotes (
|
|
107
56
|
${pkColumn},
|
|
108
57
|
user_id INTEGER,
|
|
109
58
|
upvoteable_id INTEGER NOT NULL,
|
|
110
59
|
upvoteable_type VARCHAR(255) NOT NULL,
|
|
111
60
|
${createdAt(sql)}
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
export function traitTableIndexSql() {
|
|
115
|
-
return [
|
|
116
|
-
"CREATE INDEX IF NOT EXISTS commentables_owner_index ON commentables (commentables_type, commentables_id)",
|
|
117
|
-
"CREATE INDEX IF NOT EXISTS commentables_status_index ON commentables (status)",
|
|
118
|
-
"CREATE UNIQUE INDEX IF NOT EXISTS taggables_owner_slug_unique ON taggables (taggable_type, taggable_id, slug)",
|
|
119
|
-
"CREATE UNIQUE INDEX IF NOT EXISTS categorizables_owner_slug_unique ON categorizables (categorizable_type, categorizable_id, slug)",
|
|
120
|
-
"CREATE INDEX IF NOT EXISTS commentable_upvotes_target_index ON commentable_upvotes (upvoteable_type, upvoteable_id)",
|
|
121
|
-
"CREATE UNIQUE INDEX IF NOT EXISTS commentable_upvotes_user_unique ON commentable_upvotes (upvoteable_type, upvoteable_id, user_id)",
|
|
122
|
-
"CREATE UNIQUE INDEX IF NOT EXISTS taggable_models_unique ON taggable_models (tag_id, taggable_id, taggable_type)",
|
|
123
|
-
"CREATE UNIQUE INDEX IF NOT EXISTS categorizable_models_unique ON categorizable_models (category_id, categorizable_id, categorizable_type)"
|
|
124
|
-
];
|
|
125
|
-
}
|
|
126
|
-
export async function likeableTargets() {
|
|
127
|
-
const { path } = await import("@stacksjs/path"), { globSync } = await import("@stacksjs/storage"), { getTableName } = await import("@stacksjs/orm"), { getLikeableForeignKey, getUpvoteTableName } = await import("./drivers/helpers"), modelFiles = globSync([path.userModelsPath("*.ts"), path.storagePath("framework/defaults/app/Models/**/*.ts")], { absolute: !0 }), targets = new Map;
|
|
128
|
-
for (const modelFile of modelFiles) {
|
|
129
|
-
let model;
|
|
130
|
-
try {
|
|
131
|
-
model = (await import(modelFile)).default;
|
|
132
|
-
} catch {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (!model?.traits?.likeable)
|
|
136
|
-
continue;
|
|
137
|
-
const tableName = await getTableName(model, modelFile);
|
|
138
|
-
if (!tableName)
|
|
139
|
-
continue;
|
|
140
|
-
const table = getUpvoteTableName(model, tableName);
|
|
141
|
-
if (!table || !/^[a-z_]\w*$/i.test(table))
|
|
142
|
-
continue;
|
|
143
|
-
const foreignKey = getLikeableForeignKey(model, tableName);
|
|
144
|
-
if (!/^[a-z_]\w*$/i.test(foreignKey))
|
|
145
|
-
continue;
|
|
146
|
-
targets.set(table, { table, foreignKey });
|
|
147
|
-
}
|
|
148
|
-
return [...targets.values()];
|
|
149
|
-
}
|
|
150
|
-
export function indexSqlForDialect(statement, dialect) {
|
|
151
|
-
if (dialectCapabilities(dialect).supportsCreateIndexIfNotExists)
|
|
152
|
-
return statement;
|
|
153
|
-
return statement.replace(/^(CREATE (?:UNIQUE )?INDEX) IF NOT EXISTS /i, "$1 ");
|
|
154
|
-
}
|
|
155
|
-
function isDuplicateIndexError(error) {
|
|
156
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
157
|
-
return /duplicate key name|already exists/i.test(message);
|
|
158
|
-
}
|
|
159
|
-
export async function migrateTraitTables(options = {}) {
|
|
160
|
-
const dbDriver = getDbDriver(), sql = sqlHelpers(dbDriver);
|
|
161
|
-
if (options.verbose)
|
|
162
|
-
log.info(`Creating polymorphic trait tables for ${dbDriver}...`);
|
|
163
|
-
try {
|
|
164
|
-
if (options.verbose)
|
|
165
|
-
log.info("Creating commentables table...");
|
|
166
|
-
await db.unsafe(commentablesTableSql(sql)).execute();
|
|
167
|
-
if (options.verbose)
|
|
168
|
-
log.info("Creating taggables table...");
|
|
169
|
-
await db.unsafe(taggablesTableSql(sql)).execute();
|
|
170
|
-
if (options.verbose)
|
|
171
|
-
log.info("Creating categorizables table...");
|
|
172
|
-
await db.unsafe(categorizablesTableSql(sql)).execute();
|
|
173
|
-
if (options.verbose)
|
|
174
|
-
log.info("Creating commentable_upvotes table...");
|
|
175
|
-
await db.unsafe(commentableUpvotesTableSql(sql)).execute();
|
|
176
|
-
if (options.verbose)
|
|
177
|
-
log.info("Creating taggable_models pivot...");
|
|
178
|
-
await db.unsafe(taggableModelsTableSql(sql)).execute();
|
|
179
|
-
if (options.verbose)
|
|
180
|
-
log.info("Creating categorizable_models pivot...");
|
|
181
|
-
await db.unsafe(categorizableModelsTableSql(sql)).execute();
|
|
182
|
-
try {
|
|
183
|
-
for (const { table, foreignKey } of await likeableTargets()) {
|
|
184
|
-
if (options.verbose)
|
|
185
|
-
log.info(`Creating ${table} table...`);
|
|
186
|
-
await db.unsafe(likesTableSql(sql, table, foreignKey)).execute();
|
|
187
|
-
}
|
|
188
|
-
} catch (error) {
|
|
189
|
-
log.debug(`[trait-tables] Skipped likeable tables: ${error instanceof Error ? error.message : String(error)}`);
|
|
190
|
-
}
|
|
191
|
-
for (const statement of traitTableIndexSql())
|
|
192
|
-
try {
|
|
193
|
-
await db.unsafe(indexSqlForDialect(statement, dbDriver)).execute();
|
|
194
|
-
} catch (error) {
|
|
195
|
-
if (!isDuplicateIndexError(error))
|
|
196
|
-
throw error;
|
|
197
|
-
}
|
|
198
|
-
if (options.verbose)
|
|
199
|
-
log.success("Polymorphic trait tables created");
|
|
200
|
-
return { success: !0 };
|
|
201
|
-
} catch (error) {
|
|
202
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
-
log.error(`Failed to create polymorphic trait tables: ${message}`);
|
|
204
|
-
return { success: !1, error: message };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
61
|
+
)`}export function traitTableIndexSql(){return["CREATE INDEX IF NOT EXISTS commentables_owner_index ON commentables (commentables_type, commentables_id)","CREATE INDEX IF NOT EXISTS commentables_status_index ON commentables (status)","CREATE UNIQUE INDEX IF NOT EXISTS taggables_owner_slug_unique ON taggables (taggable_type, taggable_id, slug)","CREATE UNIQUE INDEX IF NOT EXISTS categorizables_owner_slug_unique ON categorizables (categorizable_type, categorizable_id, slug)","CREATE INDEX IF NOT EXISTS commentable_upvotes_target_index ON commentable_upvotes (upvoteable_type, upvoteable_id)","CREATE UNIQUE INDEX IF NOT EXISTS commentable_upvotes_user_unique ON commentable_upvotes (upvoteable_type, upvoteable_id, user_id)","CREATE UNIQUE INDEX IF NOT EXISTS taggable_models_unique ON taggable_models (tag_id, taggable_id, taggable_type)","CREATE UNIQUE INDEX IF NOT EXISTS categorizable_models_unique ON categorizable_models (category_id, categorizable_id, categorizable_type)"]}export async function likeableTargets(){const{path}=await import("@stacksjs/path"),{globSync}=await import("@stacksjs/storage"),{getTableName}=await import("@stacksjs/orm"),{getLikeableForeignKey,getUpvoteTableName}=await import("./drivers/helpers"),modelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),targets=new Map;for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model?.traits?.likeable)continue;const tableName=await getTableName(model,modelFile);if(!tableName)continue;const table=getUpvoteTableName(model,tableName);if(!table||!/^[a-z_]\w*$/i.test(table))continue;const foreignKey=getLikeableForeignKey(model,tableName);if(!/^[a-z_]\w*$/i.test(foreignKey))continue;targets.set(table,{table,foreignKey})}return[...targets.values()]}export function indexSqlForDialect(statement,dialect){if(dialectCapabilities(dialect).supportsCreateIndexIfNotExists)return statement;return statement.replace(/^(CREATE (?:UNIQUE )?INDEX) IF NOT EXISTS /i,"$1 ")}function isDuplicateIndexError(error){const message=error instanceof Error?error.message:String(error);return/duplicate key name|already exists/i.test(message)}export async function migrateTraitTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver);if(options.verbose)log.info(`Creating polymorphic trait tables for ${dbDriver}...`);try{if(options.verbose)log.info("Creating commentables table...");await db.unsafe(commentablesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggables table...");await db.unsafe(taggablesTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizables table...");await db.unsafe(categorizablesTableSql(sql)).execute();if(options.verbose)log.info("Creating commentable_upvotes table...");await db.unsafe(commentableUpvotesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggable_models pivot...");await db.unsafe(taggableModelsTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizable_models pivot...");await db.unsafe(categorizableModelsTableSql(sql)).execute();try{for(const{table,foreignKey}of await likeableTargets()){if(options.verbose)log.info(`Creating ${table} table...`);await db.unsafe(likesTableSql(sql,table,foreignKey)).execute()}}catch(error){log.debug(`[trait-tables] Skipped likeable tables: ${error instanceof Error?error.message:String(error)}`)}for(const statement of traitTableIndexSql())try{await db.unsafe(indexSqlForDialect(statement,dbDriver)).execute()}catch(error){if(!isDuplicateIndexError(error))throw error}if(options.verbose)log.success("Polymorphic trait tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create polymorphic trait tables: ${message}`);return{success:!1,error:message}}}
|
|
@@ -1,62 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
const transactionStorage = new AsyncLocalStorage;
|
|
3
|
-
export function isInTransaction() {
|
|
4
|
-
return transactionStorage.getStore() !== void 0;
|
|
5
|
-
}
|
|
6
|
-
export function enqueueAfterCommit(callback) {
|
|
7
|
-
const scope = transactionStorage.getStore();
|
|
8
|
-
if (!scope)
|
|
9
|
-
return !1;
|
|
10
|
-
scope.pending.push(callback);
|
|
11
|
-
return !0;
|
|
12
|
-
}
|
|
13
|
-
export async function runInTransactionScope(fn, options = {}) {
|
|
14
|
-
const existing = transactionStorage.getStore();
|
|
15
|
-
if (existing) {
|
|
16
|
-
existing.depth += 1;
|
|
17
|
-
try {
|
|
18
|
-
return await fn();
|
|
19
|
-
} finally {
|
|
20
|
-
existing.depth -= 1;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
const scope = {
|
|
24
|
-
pending: [],
|
|
25
|
-
depth: 1,
|
|
26
|
-
onError: options.onError
|
|
27
|
-
};
|
|
28
|
-
let result;
|
|
29
|
-
try {
|
|
30
|
-
result = await transactionStorage.run(scope, fn);
|
|
31
|
-
} catch (err) {
|
|
32
|
-
scope.pending.length = 0;
|
|
33
|
-
throw err;
|
|
34
|
-
}
|
|
35
|
-
await flushScope(scope);
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
async function flushScope(scope) {
|
|
39
|
-
for (let i = 0;i < scope.pending.length; i++)
|
|
40
|
-
try {
|
|
41
|
-
await scope.pending[i]();
|
|
42
|
-
} catch (err) {
|
|
43
|
-
if (scope.onError)
|
|
44
|
-
try {
|
|
45
|
-
scope.onError(err, i);
|
|
46
|
-
} catch {}
|
|
47
|
-
else
|
|
48
|
-
console.error("[transaction-context] after-commit callback threw:", err);
|
|
49
|
-
}
|
|
50
|
-
scope.pending.length = 0;
|
|
51
|
-
}
|
|
52
|
-
export async function __flushAfterCommitNow() {
|
|
53
|
-
const scope = transactionStorage.getStore();
|
|
54
|
-
if (!scope)
|
|
55
|
-
return 0;
|
|
56
|
-
const count = scope.pending.length;
|
|
57
|
-
await flushScope(scope);
|
|
58
|
-
return count;
|
|
59
|
-
}
|
|
60
|
-
export function __pendingAfterCommitCount() {
|
|
61
|
-
return transactionStorage.getStore()?.pending.length ?? 0;
|
|
62
|
-
}
|
|
1
|
+
import{AsyncLocalStorage}from"node:async_hooks";const transactionStorage=new AsyncLocalStorage;export function isInTransaction(){return transactionStorage.getStore()!==void 0}export function enqueueAfterCommit(callback){const scope=transactionStorage.getStore();if(!scope)return!1;scope.pending.push(callback);return!0}export async function runInTransactionScope(fn,options={}){const existing=transactionStorage.getStore();if(existing){existing.depth+=1;try{return await fn()}finally{existing.depth-=1}}const scope={pending:[],depth:1,onError:options.onError};let result;try{result=await transactionStorage.run(scope,fn)}catch(err){scope.pending.length=0;throw err}await flushScope(scope);return result}async function flushScope(scope){for(let i=0;i<scope.pending.length;i++)try{await scope.pending[i]()}catch(err){if(scope.onError)try{scope.onError(err,i)}catch{}else console.error("[transaction-context] after-commit callback threw:",err)}scope.pending.length=0}export async function __flushAfterCommitNow(){const scope=transactionStorage.getStore();if(!scope)return 0;const count=scope.pending.length;await flushScope(scope);return count}export function __pendingAfterCommitCount(){return transactionStorage.getStore()?.pending.length??0}
|
package/dist/types.js
CHANGED
|
@@ -1,98 +1 @@
|
|
|
1
|
-
const SAFE_ALIAS
|
|
2
|
-
function assertSqlTextIdentifier(value, kind) {
|
|
3
|
-
if (!(kind === "column" ? SAFE_COLUMN : SAFE_ALIAS).test(value))
|
|
4
|
-
throw TypeError(`[database] refusing to interpolate unsafe ${kind} ${JSON.stringify(value)} into SQL text - expected a plain identifier (letters, digits, underscores)`);
|
|
5
|
-
}
|
|
6
|
-
function createSqlFragment(text, parameters) {
|
|
7
|
-
return {
|
|
8
|
-
sql: text,
|
|
9
|
-
parameters,
|
|
10
|
-
as(alias) {
|
|
11
|
-
assertSqlTextIdentifier(alias, "alias");
|
|
12
|
-
return createSqlFragment(`${text} AS ${alias}`, parameters);
|
|
13
|
-
},
|
|
14
|
-
toString() {
|
|
15
|
-
return text;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
export function sql(strings, ...values) {
|
|
20
|
-
const sqlParts = [], parameters = [];
|
|
21
|
-
for (let i = 0;i < strings.length; i++) {
|
|
22
|
-
sqlParts.push(strings[i]);
|
|
23
|
-
if (i < values.length)
|
|
24
|
-
if (values[i] && typeof values[i] === "object" && "raw" in values[i])
|
|
25
|
-
sqlParts.push(values[i].raw);
|
|
26
|
-
else {
|
|
27
|
-
sqlParts.push("?");
|
|
28
|
-
parameters.push(values[i]);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return createSqlFragment(sqlParts.join(""), parameters);
|
|
32
|
-
}
|
|
33
|
-
sql.raw = function raw(value) {
|
|
34
|
-
return { raw: value };
|
|
35
|
-
};
|
|
36
|
-
sql.ref = function ref(column) {
|
|
37
|
-
return { raw: column };
|
|
38
|
-
};
|
|
39
|
-
sql.literal = function literal(value) {
|
|
40
|
-
return { raw: inlineSqlLiteral(value) };
|
|
41
|
-
};
|
|
42
|
-
const SAFE_FILTER_OPERATORS = new Set([
|
|
43
|
-
"=",
|
|
44
|
-
"!=",
|
|
45
|
-
"<>",
|
|
46
|
-
"<",
|
|
47
|
-
"<=",
|
|
48
|
-
">",
|
|
49
|
-
">=",
|
|
50
|
-
"like",
|
|
51
|
-
"not like",
|
|
52
|
-
"ilike",
|
|
53
|
-
"not ilike",
|
|
54
|
-
"is",
|
|
55
|
-
"is not"
|
|
56
|
-
]);
|
|
57
|
-
function inlineSqlLiteral(value) {
|
|
58
|
-
if (value === null || value === void 0)
|
|
59
|
-
return "NULL";
|
|
60
|
-
if (typeof value === "number") {
|
|
61
|
-
if (!Number.isFinite(value))
|
|
62
|
-
throw TypeError(`[database] refusing to inline non-finite number into SQL: ${value}`);
|
|
63
|
-
return String(value);
|
|
64
|
-
}
|
|
65
|
-
if (typeof value === "boolean")
|
|
66
|
-
return value ? "1" : "0";
|
|
67
|
-
if (typeof value === "bigint")
|
|
68
|
-
return value.toString();
|
|
69
|
-
return `'${String(value).replace(/'/g, "''")}'`;
|
|
70
|
-
}
|
|
71
|
-
function createAggregateExpression(text) {
|
|
72
|
-
return {
|
|
73
|
-
sql: text,
|
|
74
|
-
as(alias) {
|
|
75
|
-
assertSqlTextIdentifier(alias, "alias");
|
|
76
|
-
return createAggregateExpression(`${text} AS ${alias}`);
|
|
77
|
-
},
|
|
78
|
-
filterWhere(column, op, value) {
|
|
79
|
-
assertSqlTextIdentifier(column, "column");
|
|
80
|
-
if (!SAFE_FILTER_OPERATORS.has(op.toLowerCase()))
|
|
81
|
-
throw TypeError(`[database] refusing unsafe aggregate filter operator ${JSON.stringify(op)} - allowed: ${[...SAFE_FILTER_OPERATORS].join(", ")}`);
|
|
82
|
-
return createAggregateExpression(`${text} FILTER (WHERE ${column} ${op} ${inlineSqlLiteral(value)})`);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function aggregate(name, column) {
|
|
87
|
-
if (column !== void 0)
|
|
88
|
-
assertSqlTextIdentifier(column, "column");
|
|
89
|
-
return createAggregateExpression(column === void 0 ? `${name}(*)` : `${name}(${column})`);
|
|
90
|
-
}
|
|
91
|
-
export const aggregateFunctions = {
|
|
92
|
-
countAll: () => aggregate("COUNT"),
|
|
93
|
-
count: (column) => aggregate("COUNT", column),
|
|
94
|
-
sum: (column) => aggregate("SUM", column),
|
|
95
|
-
avg: (column) => aggregate("AVG", column),
|
|
96
|
-
min: (column) => aggregate("MIN", column),
|
|
97
|
-
max: (column) => aggregate("MAX", column)
|
|
98
|
-
};
|
|
1
|
+
const SAFE_ALIAS=/^[A-Z_][A-Z0-9_]*$/i,SAFE_COLUMN=/^[A-Z_][A-Z0-9_]*(\.[A-Z_][A-Z0-9_]*)?$/i;function assertSqlTextIdentifier(value,kind){if(!(kind==="column"?SAFE_COLUMN:SAFE_ALIAS).test(value))throw TypeError(`[database] refusing to interpolate unsafe ${kind} ${JSON.stringify(value)} into SQL text - expected a plain identifier (letters, digits, underscores)`)}function createSqlFragment(text,parameters){return{sql:text,parameters,as(alias){assertSqlTextIdentifier(alias,"alias");return createSqlFragment(`${text} AS ${alias}`,parameters)},toString(){return text}}}export function sql(strings,...values){const sqlParts=[],parameters=[];for(let i=0;i<strings.length;i++){sqlParts.push(strings[i]);if(i<values.length)if(values[i]&&typeof values[i]==="object"&&"raw"in values[i])sqlParts.push(values[i].raw);else{sqlParts.push("?");parameters.push(values[i])}}return createSqlFragment(sqlParts.join(""),parameters)}sql.raw=function raw(value){return{raw:value}};sql.ref=function ref(column){return{raw:column}};sql.literal=function literal(value){return{raw:inlineSqlLiteral(value)}};const SAFE_FILTER_OPERATORS=new Set(["=","!=","<>","<","<=",">",">=","like","not like","ilike","not ilike","is","is not"]);function inlineSqlLiteral(value){if(value===null||value===void 0)return"NULL";if(typeof value==="number"){if(!Number.isFinite(value))throw TypeError(`[database] refusing to inline non-finite number into SQL: ${value}`);return String(value)}if(typeof value==="boolean")return value?"1":"0";if(typeof value==="bigint")return value.toString();return`'${String(value).replace(/'/g,"''")}'`}function createAggregateExpression(text){return{sql:text,as(alias){assertSqlTextIdentifier(alias,"alias");return createAggregateExpression(`${text} AS ${alias}`)},filterWhere(column,op,value){assertSqlTextIdentifier(column,"column");if(!SAFE_FILTER_OPERATORS.has(op.toLowerCase()))throw TypeError(`[database] refusing unsafe aggregate filter operator ${JSON.stringify(op)} - allowed: ${[...SAFE_FILTER_OPERATORS].join(", ")}`);return createAggregateExpression(`${text} FILTER (WHERE ${column} ${op} ${inlineSqlLiteral(value)})`)}}}function aggregate(name,column){if(column!==void 0)assertSqlTextIdentifier(column,"column");return createAggregateExpression(column===void 0?`${name}(*)`:`${name}(${column})`)}export const aggregateFunctions={countAll:()=>aggregate("COUNT"),count:(column)=>aggregate("COUNT",column),sum:(column)=>aggregate("SUM",column),avg:(column)=>aggregate("AVG",column),min:(column)=>aggregate("MIN",column),max:(column)=>aggregate("MAX",column)};
|
package/dist/unique-audit.js
CHANGED
|
@@ -1,142 +1,10 @@
|
|
|
1
|
-
import { path }
|
|
2
|
-
import { plural, snakeCase } from "@stacksjs/strings";
|
|
3
|
-
import { safeGlob } from "./fk-audit";
|
|
4
|
-
export async function getDeclaredUniques() {
|
|
5
|
-
const modelFiles = [
|
|
6
|
-
...safeGlob(path.userModelsPath("*.ts")),
|
|
7
|
-
...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))
|
|
8
|
-
], declared = [];
|
|
9
|
-
for (const modelFile of modelFiles) {
|
|
10
|
-
let model;
|
|
11
|
-
try {
|
|
12
|
-
model = (await import(modelFile)).default;
|
|
13
|
-
} catch {
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
if (!model || typeof model !== "object")
|
|
17
|
-
continue;
|
|
18
|
-
const table = model.table || plural(snakeCase(model.name || "")), modelName = model.name || "", attributes = model.attributes;
|
|
19
|
-
if (attributes && typeof attributes === "object") {
|
|
20
|
-
for (const [field, attr] of Object.entries(attributes))
|
|
21
|
-
if (attr && typeof attr === "object" && attr.unique === !0)
|
|
22
|
-
declared.push({
|
|
23
|
-
table,
|
|
24
|
-
columns: [snakeCase(field)],
|
|
25
|
-
model: modelName,
|
|
26
|
-
source: "attribute"
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
const indexes = model.indexes;
|
|
30
|
-
if (Array.isArray(indexes)) {
|
|
31
|
-
for (const index of indexes)
|
|
32
|
-
if (index && index.unique === !0 && Array.isArray(index.columns) && index.columns.length > 0)
|
|
33
|
-
declared.push({
|
|
34
|
-
table,
|
|
35
|
-
columns: index.columns.map((c) => snakeCase(c)),
|
|
36
|
-
model: modelName,
|
|
37
|
-
source: "index",
|
|
38
|
-
indexName: index.name
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return declared;
|
|
43
|
-
}
|
|
44
|
-
export async function getLiveUniqueIndexes(dialect) {
|
|
45
|
-
const { db } = await import("./utils"), d = dialect ?? await currentDialect();
|
|
46
|
-
if (d === "sqlite")
|
|
47
|
-
return getSqliteLiveUniques(db);
|
|
48
|
-
if (d === "mysql")
|
|
49
|
-
return getMysqlLiveUniques(db);
|
|
50
|
-
if (d === "postgres")
|
|
51
|
-
return getPostgresLiveUniques(db);
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
export async function auditUniqueIndexes(dialect) {
|
|
55
|
-
const d = dialect ?? await currentDialect();
|
|
56
|
-
if (d !== "sqlite" && d !== "mysql" && d !== "postgres")
|
|
57
|
-
return { supported: !1, declared: [], live: [], missing: [], skippedTables: [] };
|
|
58
|
-
const declared = await getDeclaredUniques(), live = await getLiveUniqueIndexes(d), liveTables = await getLiveTables(d), liveByTable = new Map;
|
|
59
|
-
for (const idx of live) {
|
|
60
|
-
const t = idx.table.toLowerCase(), key = columnSetKey(idx.columns);
|
|
61
|
-
if (!liveByTable.has(t))
|
|
62
|
-
liveByTable.set(t, new Set);
|
|
63
|
-
liveByTable.get(t).add(key);
|
|
64
|
-
}
|
|
65
|
-
const missing = [], skippedTables = new Set;
|
|
66
|
-
for (const decl of declared) {
|
|
67
|
-
const t = decl.table.toLowerCase();
|
|
68
|
-
if (!liveTables.has(t)) {
|
|
69
|
-
skippedTables.add(decl.table);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const key = columnSetKey(decl.columns);
|
|
73
|
-
if (!(liveByTable.get(t)?.has(key) ?? !1))
|
|
74
|
-
missing.push(decl);
|
|
75
|
-
}
|
|
76
|
-
return { supported: !0, declared, live, missing, skippedTables: [...skippedTables] };
|
|
77
|
-
}
|
|
78
|
-
async function getLiveTables(dialect) {
|
|
79
|
-
const { db } = await import("./utils");
|
|
80
|
-
let rows = [];
|
|
81
|
-
if (dialect === "sqlite") {
|
|
82
|
-
const r = await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute();
|
|
83
|
-
rows = (Array.isArray(r) ? r : []).map((x) => x.name);
|
|
84
|
-
} else if (dialect === "mysql") {
|
|
85
|
-
const r = await db.unsafe("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()").execute();
|
|
86
|
-
rows = (Array.isArray(r) ? r : []).map((x) => x.name ?? x.TABLE_NAME);
|
|
87
|
-
} else if (dialect === "postgres") {
|
|
88
|
-
const r = await db.unsafe("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'").execute();
|
|
89
|
-
rows = (Array.isArray(r) ? r : []).map((x) => x.name ?? x.tablename);
|
|
90
|
-
}
|
|
91
|
-
return new Set(rows.filter((n) => typeof n === "string" && n.length > 0).map((n) => n.toLowerCase()));
|
|
92
|
-
}
|
|
93
|
-
function columnSetKey(columns) {
|
|
94
|
-
return [...columns].map((c) => c.toLowerCase()).sort().join(",");
|
|
95
|
-
}
|
|
96
|
-
async function currentDialect() {
|
|
97
|
-
const driver = ((await import("@stacksjs/env")).env?.DB_CONNECTION ?? "sqlite").toLowerCase();
|
|
98
|
-
if (driver === "sqlite" || driver === "mysql" || driver === "postgres")
|
|
99
|
-
return driver;
|
|
100
|
-
return "other";
|
|
101
|
-
}
|
|
102
|
-
async function getSqliteLiveUniques(db) {
|
|
103
|
-
const tables = await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute(), rows = Array.isArray(tables) ? tables : [], out = [];
|
|
104
|
-
for (const row of rows) {
|
|
105
|
-
const table = row.name;
|
|
106
|
-
if (!table)
|
|
107
|
-
continue;
|
|
108
|
-
if (!/^[a-z_]\w*$/i.test(table))
|
|
109
|
-
continue;
|
|
110
|
-
const indexRows = await db.unsafe(`PRAGMA index_list("${table}")`).execute();
|
|
111
|
-
for (const idx of Array.isArray(indexRows) ? indexRows : []) {
|
|
112
|
-
const r = idx;
|
|
113
|
-
if (Number(r.unique) !== 1 || !r.name)
|
|
114
|
-
continue;
|
|
115
|
-
if (!/^[a-z_]\w*$/i.test(r.name))
|
|
116
|
-
continue;
|
|
117
|
-
const colRows = await db.unsafe(`PRAGMA index_info("${r.name}")`).execute(), columns = (Array.isArray(colRows) ? colRows : []).map((c) => String(c.name ?? "")).filter((c) => c.length > 0);
|
|
118
|
-
if (columns.length > 0)
|
|
119
|
-
out.push({ table, name: r.name, columns });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
async function getMysqlLiveUniques(db) {
|
|
125
|
-
const rows = await db.unsafe(`
|
|
1
|
+
import{path}from"@stacksjs/path";import{plural,snakeCase}from"@stacksjs/strings";import{safeGlob}from"./fk-audit";export async function getDeclaredUniques(){const modelFiles=[...safeGlob(path.userModelsPath("*.ts")),...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))],declared=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model||typeof model!=="object")continue;const table=model.table||plural(snakeCase(model.name||"")),modelName=model.name||"",attributes=model.attributes;if(attributes&&typeof attributes==="object"){for(const[field,attr]of Object.entries(attributes))if(attr&&typeof attr==="object"&&attr.unique===!0)declared.push({table,columns:[snakeCase(field)],model:modelName,source:"attribute"})}const indexes=model.indexes;if(Array.isArray(indexes)){for(const index of indexes)if(index&&index.unique===!0&&Array.isArray(index.columns)&&index.columns.length>0)declared.push({table,columns:index.columns.map((c)=>snakeCase(c)),model:modelName,source:"index",indexName:index.name})}}return declared}export async function getLiveUniqueIndexes(dialect){const{db}=await import("./utils"),d=dialect??await currentDialect();if(d==="sqlite")return getSqliteLiveUniques(db);if(d==="mysql")return getMysqlLiveUniques(db);if(d==="postgres")return getPostgresLiveUniques(db);return[]}export async function auditUniqueIndexes(dialect){const d=dialect??await currentDialect();if(d!=="sqlite"&&d!=="mysql"&&d!=="postgres")return{supported:!1,declared:[],live:[],missing:[],skippedTables:[]};const declared=await getDeclaredUniques(),live=await getLiveUniqueIndexes(d),liveTables=await getLiveTables(d),liveByTable=new Map;for(const idx of live){const t=idx.table.toLowerCase(),key=columnSetKey(idx.columns);if(!liveByTable.has(t))liveByTable.set(t,new Set);liveByTable.get(t).add(key)}const missing=[],skippedTables=new Set;for(const decl of declared){const t=decl.table.toLowerCase();if(!liveTables.has(t)){skippedTables.add(decl.table);continue}const key=columnSetKey(decl.columns);if(!(liveByTable.get(t)?.has(key)??!1))missing.push(decl)}return{supported:!0,declared,live,missing,skippedTables:[...skippedTables]}}async function getLiveTables(dialect){const{db}=await import("./utils");let rows=[];if(dialect==="sqlite"){const r=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name)}else if(dialect==="mysql"){const r=await db.unsafe("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.TABLE_NAME)}else if(dialect==="postgres"){const r=await db.unsafe("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.tablename)}return new Set(rows.filter((n)=>typeof n==="string"&&n.length>0).map((n)=>n.toLowerCase()))}function columnSetKey(columns){return[...columns].map((c)=>c.toLowerCase()).sort().join(",")}async function currentDialect(){const driver=((await import("@stacksjs/env")).env?.DB_CONNECTION??"sqlite").toLowerCase();if(driver==="sqlite"||driver==="mysql"||driver==="postgres")return driver;return"other"}async function getSqliteLiveUniques(db){const tables=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute(),rows=Array.isArray(tables)?tables:[],out=[];for(const row of rows){const table=row.name;if(!table)continue;if(!/^[a-z_]\w*$/i.test(table))continue;const indexRows=await db.unsafe(`PRAGMA index_list("${table}")`).execute();for(const idx of Array.isArray(indexRows)?indexRows:[]){const r=idx;if(Number(r.unique)!==1||!r.name)continue;if(!/^[a-z_]\w*$/i.test(r.name))continue;const colRows=await db.unsafe(`PRAGMA index_info("${r.name}")`).execute(),columns=(Array.isArray(colRows)?colRows:[]).map((c)=>String(c.name??"")).filter((c)=>c.length>0);if(columns.length>0)out.push({table,name:r.name,columns})}}return out}async function getMysqlLiveUniques(db){const rows=await db.unsafe(`
|
|
126
2
|
SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX
|
|
127
3
|
FROM information_schema.STATISTICS
|
|
128
4
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
129
5
|
AND NON_UNIQUE = 0
|
|
130
6
|
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
|
|
131
|
-
`).execute();
|
|
132
|
-
return groupIndexRows(Array.isArray(rows) ? rows : [], (r) => ({
|
|
133
|
-
table: String(r.TABLE_NAME ?? r.table_name ?? ""),
|
|
134
|
-
name: String(r.INDEX_NAME ?? r.index_name ?? ""),
|
|
135
|
-
column: String(r.COLUMN_NAME ?? r.column_name ?? "")
|
|
136
|
-
}));
|
|
137
|
-
}
|
|
138
|
-
async function getPostgresLiveUniques(db) {
|
|
139
|
-
const rows = await db.unsafe(`
|
|
7
|
+
`).execute();return groupIndexRows(Array.isArray(rows)?rows:[],(r)=>({table:String(r.TABLE_NAME??r.table_name??""),name:String(r.INDEX_NAME??r.index_name??""),column:String(r.COLUMN_NAME??r.column_name??"")}))}async function getPostgresLiveUniques(db){const rows=await db.unsafe(`
|
|
140
8
|
SELECT
|
|
141
9
|
t.relname AS table_name,
|
|
142
10
|
ix.relname AS index_name,
|
|
@@ -151,24 +19,4 @@ async function getPostgresLiveUniques(db) {
|
|
|
151
19
|
WHERE i.indisunique = true
|
|
152
20
|
AND n.nspname = 'public'
|
|
153
21
|
ORDER BY table_name, index_name, seq_in_index
|
|
154
|
-
`).execute();
|
|
155
|
-
return groupIndexRows(Array.isArray(rows) ? rows : [], (r) => ({
|
|
156
|
-
table: String(r.table_name ?? ""),
|
|
157
|
-
name: String(r.index_name ?? ""),
|
|
158
|
-
column: String(r.column_name ?? "")
|
|
159
|
-
}));
|
|
160
|
-
}
|
|
161
|
-
function groupIndexRows(rows, pick) {
|
|
162
|
-
const map = new Map;
|
|
163
|
-
for (const row of rows) {
|
|
164
|
-
const { table, name, column } = pick(row);
|
|
165
|
-
if (!table || !name || !column)
|
|
166
|
-
continue;
|
|
167
|
-
const key = `${table} ${name}`, existing = map.get(key);
|
|
168
|
-
if (existing)
|
|
169
|
-
existing.columns.push(column);
|
|
170
|
-
else
|
|
171
|
-
map.set(key, { table, name, columns: [column] });
|
|
172
|
-
}
|
|
173
|
-
return [...map.values()];
|
|
174
|
-
}
|
|
22
|
+
`).execute();return groupIndexRows(Array.isArray(rows)?rows:[],(r)=>({table:String(r.table_name??""),name:String(r.index_name??""),column:String(r.column_name??"")}))}function groupIndexRows(rows,pick){const map=new Map;for(const row of rows){const{table,name,column}=pick(row);if(!table||!name||!column)continue;const key=`${table} ${name}`,existing=map.get(key);if(existing)existing.columns.push(column);else map.set(key,{table,name,columns:[column]})}return[...map.values()]}
|