@nexa-stack/framework 1.0.0
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/.env.example +46 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/bin/nexa.mjs +41 -0
- package/bin/nexa.ts +334 -0
- package/docs/AI.md +69 -0
- package/docs/ARCHITECTURE.md +74 -0
- package/docs/EXAMPLES.md +114 -0
- package/docs/FRAMEWORK.md +226 -0
- package/docs/LANGUAGE.md +39 -0
- package/docs/README.md +7 -0
- package/docs/READY.md +51 -0
- package/docs/REFERENCE.md +255 -0
- package/docs/START.md +98 -0
- package/docs/advanced.md +97 -0
- package/docs/authentication.md +54 -0
- package/docs/cli.md +15 -0
- package/docs/compare.md +51 -0
- package/docs/configuration.md +65 -0
- package/docs/database.md +396 -0
- package/docs/installation.md +57 -0
- package/docs/localization.md +47 -0
- package/docs/resources.md +75 -0
- package/docs/routing.md +59 -0
- package/docs/seeding.md +33 -0
- package/docs/services.md +146 -0
- package/package.json +77 -0
- package/packages/auth/src/auth.test.ts +23 -0
- package/packages/auth/src/auth.ts +287 -0
- package/packages/auth/src/index.ts +17 -0
- package/packages/cache/src/index.ts +203 -0
- package/packages/client/src/index.ts +49 -0
- package/packages/core/src/app.ts +97 -0
- package/packages/core/src/config.ts +55 -0
- package/packages/core/src/dev.ts +104 -0
- package/packages/core/src/fields.test.ts +81 -0
- package/packages/core/src/fields.ts +309 -0
- package/packages/core/src/index.ts +60 -0
- package/packages/core/src/lang.ts +79 -0
- package/packages/core/src/loader.ts +42 -0
- package/packages/core/src/migrate.ts +91 -0
- package/packages/core/src/policy.ts +36 -0
- package/packages/core/src/registry.ts +17 -0
- package/packages/core/src/reload.ts +92 -0
- package/packages/core/src/resource.test.ts +32 -0
- package/packages/core/src/resource.ts +87 -0
- package/packages/core/src/routes.ts +22 -0
- package/packages/core/src/runtime.ts +11 -0
- package/packages/database/src/builder.ts +266 -0
- package/packages/database/src/database.ts +252 -0
- package/packages/database/src/dialect.ts +186 -0
- package/packages/database/src/index.ts +6 -0
- package/packages/database/src/mysql.ts +114 -0
- package/packages/database/src/postgres.ts +117 -0
- package/packages/database/src/query.ts +115 -0
- package/packages/database/src/sqlite.ts +216 -0
- package/packages/database/src/types.ts +104 -0
- package/packages/events/src/index.ts +17 -0
- package/packages/export/src/index.ts +36 -0
- package/packages/log/src/index.ts +38 -0
- package/packages/mail/src/index.ts +130 -0
- package/packages/notifications/src/index.ts +84 -0
- package/packages/plugins/src/index.ts +39 -0
- package/packages/queue/src/index.ts +185 -0
- package/packages/queue/src/jobs.ts +9 -0
- package/packages/schedule/src/index.ts +64 -0
- package/packages/server/src/index.ts +1 -0
- package/packages/server/src/middleware.ts +143 -0
- package/packages/server/src/query.ts +40 -0
- package/packages/server/src/router.ts +813 -0
- package/packages/sms/src/index.ts +33 -0
- package/packages/storage/src/upload.ts +36 -0
- package/packages/testing/src/index.ts +67 -0
- package/packages/validation/src/index.ts +1 -0
- package/packages/validation/src/validate.test.ts +35 -0
- package/packages/validation/src/validate.ts +112 -0
- package/public/admin.html +369 -0
- package/public/compare.html +66 -0
- package/public/dev-bar.js +213 -0
- package/public/docs.html +315 -0
- package/public/index.html +66 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { ResourceDefinition } from "../../core/src/resource.js";
|
|
2
|
+
import { SqliteDriver, searchFieldsFor } from "./sqlite.js";
|
|
3
|
+
import { PostgresDriver } from "./postgres.js";
|
|
4
|
+
import { MysqlDriver } from "./mysql.js";
|
|
5
|
+
import type { QueryOpts, PaginatedResult } from "./query.js";
|
|
6
|
+
import { paginate, safeIdent } from "./query.js";
|
|
7
|
+
import type { DatabaseDriver, DbRow, DbDialect } from "./types.js";
|
|
8
|
+
import { QueryBuilder } from "./builder.js";
|
|
9
|
+
import {
|
|
10
|
+
fieldColumn,
|
|
11
|
+
foreignKeyFor,
|
|
12
|
+
isStoredField,
|
|
13
|
+
pivotTableFor,
|
|
14
|
+
type FieldDef,
|
|
15
|
+
} from "../../core/src/fields.js";
|
|
16
|
+
import { sqlType, pgSqlType, mySqlType } from "./types.js";
|
|
17
|
+
|
|
18
|
+
function detectDialect(connection: string): DbDialect {
|
|
19
|
+
if (connection.startsWith("postgres://") || connection.startsWith("postgresql://")) {
|
|
20
|
+
return "postgres";
|
|
21
|
+
}
|
|
22
|
+
if (connection.startsWith("mysql://") || connection.startsWith("mariadb://")) {
|
|
23
|
+
return "mysql";
|
|
24
|
+
}
|
|
25
|
+
return "sqlite";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Database {
|
|
29
|
+
private driver: DatabaseDriver;
|
|
30
|
+
|
|
31
|
+
constructor(connection: string) {
|
|
32
|
+
const dialect = detectDialect(connection);
|
|
33
|
+
if (dialect === "postgres") {
|
|
34
|
+
this.driver = new PostgresDriver(connection);
|
|
35
|
+
} else if (dialect === "mysql") {
|
|
36
|
+
this.driver = new MysqlDriver(connection);
|
|
37
|
+
} else {
|
|
38
|
+
this.driver = new SqliteDriver(connection);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get dialect(): DbDialect {
|
|
43
|
+
return this.driver.dialect;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async connect(): Promise<void> {
|
|
47
|
+
await this.driver.connect();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
from(table: string): QueryBuilder {
|
|
51
|
+
return new QueryBuilder(this, table);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
table(table: string): QueryBuilder {
|
|
55
|
+
return this.from(table);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async transaction<T>(fn: (tx: Database) => Promise<T>): Promise<T> {
|
|
59
|
+
await this.run("BEGIN");
|
|
60
|
+
try {
|
|
61
|
+
const result = await fn(this);
|
|
62
|
+
await this.run("COMMIT");
|
|
63
|
+
return result;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
try {
|
|
66
|
+
await this.run("ROLLBACK");
|
|
67
|
+
} catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
70
|
+
throw e;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async createTable(resource: ResourceDefinition): Promise<void> {
|
|
75
|
+
await this.driver.createTable(resource);
|
|
76
|
+
await this.ensurePivotTables(resource);
|
|
77
|
+
await this.syncColumns(resource);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Add any missing columns defined on the resource */
|
|
81
|
+
async syncColumns(resource: ResourceDefinition): Promise<void> {
|
|
82
|
+
const existing = await this.listColumns(resource.table);
|
|
83
|
+
const existingSet = new Set(existing.map((c) => c.toLowerCase()));
|
|
84
|
+
|
|
85
|
+
for (const [name, field] of Object.entries(resource.fields)) {
|
|
86
|
+
if (!isStoredField(field)) continue;
|
|
87
|
+
const col = fieldColumn(name, field);
|
|
88
|
+
if (!col || existingSet.has(col.toLowerCase())) continue;
|
|
89
|
+
const typeSql = this.columnTypeSql(field);
|
|
90
|
+
await this.run(`ALTER TABLE ${resource.table} ADD COLUMN ${col} ${typeSql}`);
|
|
91
|
+
console.log(`Added column: ${resource.table}.${col}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (resource.softDelete && !existingSet.has("deleted_at")) {
|
|
95
|
+
const type =
|
|
96
|
+
this.dialect === "postgres"
|
|
97
|
+
? "TIMESTAMPTZ"
|
|
98
|
+
: this.dialect === "mysql"
|
|
99
|
+
? "VARCHAR(50) NULL"
|
|
100
|
+
: "TEXT";
|
|
101
|
+
await this.run(`ALTER TABLE ${resource.table} ADD COLUMN deleted_at ${type}`);
|
|
102
|
+
console.log(`Added column: ${resource.table}.deleted_at`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async listColumns(table: string): Promise<string[]> {
|
|
107
|
+
const t = safeIdent(table, "");
|
|
108
|
+
if (!t) return [];
|
|
109
|
+
if (this.dialect === "sqlite") {
|
|
110
|
+
const rows = await this.query(`PRAGMA table_info(${t})`);
|
|
111
|
+
return rows.map((r) => String(r.name));
|
|
112
|
+
}
|
|
113
|
+
if (this.dialect === "postgres") {
|
|
114
|
+
const rows = await this.query(
|
|
115
|
+
`SELECT column_name as name FROM information_schema.columns WHERE table_name = ?`,
|
|
116
|
+
[t]
|
|
117
|
+
);
|
|
118
|
+
return rows.map((r) => String(r.name));
|
|
119
|
+
}
|
|
120
|
+
const rows = await this.query(`SHOW COLUMNS FROM \`${t}\``);
|
|
121
|
+
return rows.map((r) => String(r.Field ?? r.field ?? r.name));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private columnTypeSql(field: FieldDef): string {
|
|
125
|
+
if (this.dialect === "postgres") return pgSqlType(field);
|
|
126
|
+
if (this.dialect === "mysql") return mySqlType(field);
|
|
127
|
+
return sqlType(field);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async ensurePivotTables(resource: ResourceDefinition): Promise<void> {
|
|
131
|
+
for (const [, field] of Object.entries(resource.fields)) {
|
|
132
|
+
if (field.type !== "belongsToMany" || !field.relation) continue;
|
|
133
|
+
const pivot = field.pivot ?? pivotTableFor(resource.table, field.relation);
|
|
134
|
+
const a = foreignKeyFor(resource.table);
|
|
135
|
+
const b = foreignKeyFor(field.relation);
|
|
136
|
+
if (this.dialect === "postgres") {
|
|
137
|
+
await this.run(`
|
|
138
|
+
CREATE TABLE IF NOT EXISTS ${pivot} (
|
|
139
|
+
id SERIAL PRIMARY KEY,
|
|
140
|
+
${a} INTEGER NOT NULL,
|
|
141
|
+
${b} INTEGER NOT NULL,
|
|
142
|
+
UNIQUE(${a}, ${b})
|
|
143
|
+
)
|
|
144
|
+
`);
|
|
145
|
+
} else if (this.dialect === "mysql") {
|
|
146
|
+
await this.run(`
|
|
147
|
+
CREATE TABLE IF NOT EXISTS ${pivot} (
|
|
148
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
149
|
+
${a} INT NOT NULL,
|
|
150
|
+
${b} INT NOT NULL,
|
|
151
|
+
UNIQUE KEY uq_pivot (${a}, ${b})
|
|
152
|
+
)
|
|
153
|
+
`);
|
|
154
|
+
} else {
|
|
155
|
+
await this.run(`
|
|
156
|
+
CREATE TABLE IF NOT EXISTS ${pivot} (
|
|
157
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
158
|
+
${a} INTEGER NOT NULL,
|
|
159
|
+
${b} INTEGER NOT NULL,
|
|
160
|
+
UNIQUE(${a}, ${b})
|
|
161
|
+
)
|
|
162
|
+
`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async findAll(table: string, opts?: QueryOpts): Promise<DbRow[]> {
|
|
168
|
+
return Promise.resolve(this.driver.findAll(table, opts));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async paginate(
|
|
172
|
+
table: string,
|
|
173
|
+
resource: ResourceDefinition,
|
|
174
|
+
opts: QueryOpts,
|
|
175
|
+
baseUrl: string
|
|
176
|
+
): Promise<PaginatedResult<DbRow>> {
|
|
177
|
+
const page = Math.floor((opts.offset ?? 0) / (opts.limit ?? 20)) + 1;
|
|
178
|
+
const perPage = opts.limit ?? 20;
|
|
179
|
+
const queryOpts = {
|
|
180
|
+
...opts,
|
|
181
|
+
searchFields: opts.search ? searchFieldsFor(resource) : undefined,
|
|
182
|
+
};
|
|
183
|
+
const total = await Promise.resolve(this.driver.count(table, queryOpts));
|
|
184
|
+
const items = await Promise.resolve(this.driver.findAll(table, queryOpts));
|
|
185
|
+
return paginate(items, total, page, perPage, baseUrl);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async findById(table: string, id: number): Promise<DbRow | null> {
|
|
189
|
+
return Promise.resolve(this.driver.findById(table, id));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async insert(table: string, data: Record<string, unknown>): Promise<DbRow> {
|
|
193
|
+
return Promise.resolve(this.driver.insert(table, data));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async update(
|
|
197
|
+
table: string,
|
|
198
|
+
id: number,
|
|
199
|
+
data: Record<string, unknown>
|
|
200
|
+
): Promise<DbRow | null> {
|
|
201
|
+
return Promise.resolve(this.driver.update(table, id, data));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async delete(table: string, id: number): Promise<boolean> {
|
|
205
|
+
return Promise.resolve(this.driver.delete(table, id));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async softDelete(table: string, id: number): Promise<boolean> {
|
|
209
|
+
const row = await this.findById(table, id);
|
|
210
|
+
if (!row) return false;
|
|
211
|
+
await this.update(table, id, { deleted_at: new Date().toISOString() });
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async restore(table: string, id: number): Promise<boolean> {
|
|
216
|
+
const row = await this.findById(table, id);
|
|
217
|
+
if (!row) return false;
|
|
218
|
+
await this.run(`UPDATE ${table} SET deleted_at = NULL WHERE id = ?`, [id]);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async query(sql: string, params?: unknown[]): Promise<DbRow[]> {
|
|
223
|
+
return Promise.resolve(this.driver.query(sql, params));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async getOne(sql: string, params?: unknown[]): Promise<DbRow | null> {
|
|
227
|
+
return Promise.resolve(this.driver.getOne(sql, params));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async run(sql: string, params?: unknown[]): Promise<void> {
|
|
231
|
+
await Promise.resolve(this.driver.run(sql, params));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
close(): void {
|
|
235
|
+
this.driver.close();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function createDatabase(connection: string): Database {
|
|
240
|
+
return new Database(connection);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function dbConnection(): string {
|
|
244
|
+
const url = process.env.DATABASE_URL;
|
|
245
|
+
if (url) return url;
|
|
246
|
+
return process.env.DB_PATH ?? "./app.db";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export { searchFieldsFor } from "./sqlite.js";
|
|
250
|
+
export type { QueryOpts, PaginatedResult } from "./query.js";
|
|
251
|
+
export type { DbDialect } from "./types.js";
|
|
252
|
+
export { QueryBuilder } from "./builder.js";
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { DbDialect } from "../../database/src/types.js";
|
|
2
|
+
|
|
3
|
+
export function migrationsTableSql(dialect: DbDialect): string {
|
|
4
|
+
if (dialect === "postgres") {
|
|
5
|
+
return `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS _nexa_migrations (
|
|
7
|
+
id SERIAL PRIMARY KEY,
|
|
8
|
+
name TEXT UNIQUE NOT NULL,
|
|
9
|
+
ran_at TIMESTAMPTZ DEFAULT NOW()
|
|
10
|
+
)
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
if (dialect === "mysql") {
|
|
14
|
+
return `
|
|
15
|
+
CREATE TABLE IF NOT EXISTS _nexa_migrations (
|
|
16
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
17
|
+
name VARCHAR(255) UNIQUE NOT NULL,
|
|
18
|
+
ran_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
19
|
+
)
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
return `
|
|
23
|
+
CREATE TABLE IF NOT EXISTS _nexa_migrations (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
name TEXT UNIQUE NOT NULL,
|
|
26
|
+
ran_at TEXT DEFAULT (datetime('now'))
|
|
27
|
+
)
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function usersTableSql(dialect: DbDialect): string {
|
|
32
|
+
if (dialect === "postgres") {
|
|
33
|
+
return `
|
|
34
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
35
|
+
id SERIAL PRIMARY KEY,
|
|
36
|
+
email TEXT UNIQUE NOT NULL,
|
|
37
|
+
password TEXT NOT NULL,
|
|
38
|
+
role TEXT DEFAULT 'user',
|
|
39
|
+
email_verified_at TEXT,
|
|
40
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
41
|
+
)
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
if (dialect === "mysql") {
|
|
45
|
+
return `
|
|
46
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
47
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
48
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
49
|
+
password TEXT NOT NULL,
|
|
50
|
+
role VARCHAR(50) DEFAULT 'user',
|
|
51
|
+
email_verified_at VARCHAR(50) NULL,
|
|
52
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
53
|
+
)
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
return `
|
|
57
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
email TEXT UNIQUE NOT NULL,
|
|
60
|
+
password TEXT NOT NULL,
|
|
61
|
+
role TEXT DEFAULT 'user',
|
|
62
|
+
email_verified_at TEXT,
|
|
63
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
64
|
+
)
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function passwordResetsTableSql(dialect: DbDialect): string {
|
|
69
|
+
if (dialect === "postgres") {
|
|
70
|
+
return `
|
|
71
|
+
CREATE TABLE IF NOT EXISTS password_resets (
|
|
72
|
+
id SERIAL PRIMARY KEY,
|
|
73
|
+
email TEXT NOT NULL,
|
|
74
|
+
token TEXT NOT NULL,
|
|
75
|
+
expires_at TEXT NOT NULL,
|
|
76
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
77
|
+
)
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
if (dialect === "mysql") {
|
|
81
|
+
return `
|
|
82
|
+
CREATE TABLE IF NOT EXISTS password_resets (
|
|
83
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
84
|
+
email VARCHAR(255) NOT NULL,
|
|
85
|
+
token VARCHAR(255) NOT NULL,
|
|
86
|
+
expires_at VARCHAR(50) NOT NULL,
|
|
87
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
88
|
+
)
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
return `
|
|
92
|
+
CREATE TABLE IF NOT EXISTS password_resets (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
email TEXT NOT NULL,
|
|
95
|
+
token TEXT NOT NULL,
|
|
96
|
+
expires_at TEXT NOT NULL,
|
|
97
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
98
|
+
)
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function notificationsTableSql(dialect: DbDialect): string {
|
|
103
|
+
if (dialect === "postgres") {
|
|
104
|
+
return `
|
|
105
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
106
|
+
id SERIAL PRIMARY KEY,
|
|
107
|
+
user_id INTEGER NOT NULL,
|
|
108
|
+
type TEXT NOT NULL,
|
|
109
|
+
title TEXT NOT NULL,
|
|
110
|
+
body TEXT DEFAULT '',
|
|
111
|
+
data TEXT DEFAULT '{}',
|
|
112
|
+
read_at TEXT,
|
|
113
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
114
|
+
)
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
if (dialect === "mysql") {
|
|
118
|
+
return `
|
|
119
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
120
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
121
|
+
user_id INT NOT NULL,
|
|
122
|
+
type VARCHAR(100) NOT NULL,
|
|
123
|
+
title VARCHAR(255) NOT NULL,
|
|
124
|
+
body TEXT,
|
|
125
|
+
data TEXT,
|
|
126
|
+
read_at VARCHAR(50) NULL,
|
|
127
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
128
|
+
)
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
return `
|
|
132
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
133
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
134
|
+
user_id INTEGER NOT NULL,
|
|
135
|
+
type TEXT NOT NULL,
|
|
136
|
+
title TEXT NOT NULL,
|
|
137
|
+
body TEXT DEFAULT '',
|
|
138
|
+
data TEXT DEFAULT '{}',
|
|
139
|
+
read_at TEXT,
|
|
140
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
141
|
+
)
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function jobsTableSql(dialect: DbDialect): string {
|
|
146
|
+
if (dialect === "postgres") {
|
|
147
|
+
return `
|
|
148
|
+
CREATE TABLE IF NOT EXISTS _nexa_jobs (
|
|
149
|
+
id SERIAL PRIMARY KEY,
|
|
150
|
+
name TEXT NOT NULL,
|
|
151
|
+
payload TEXT NOT NULL,
|
|
152
|
+
status TEXT DEFAULT 'pending',
|
|
153
|
+
attempts INTEGER DEFAULT 0,
|
|
154
|
+
max_tries INTEGER DEFAULT 3,
|
|
155
|
+
available_at TIMESTAMPTZ,
|
|
156
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
157
|
+
)
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
if (dialect === "mysql") {
|
|
161
|
+
return `
|
|
162
|
+
CREATE TABLE IF NOT EXISTS _nexa_jobs (
|
|
163
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
164
|
+
name VARCHAR(255) NOT NULL,
|
|
165
|
+
payload TEXT NOT NULL,
|
|
166
|
+
status VARCHAR(50) DEFAULT 'pending',
|
|
167
|
+
attempts INT DEFAULT 0,
|
|
168
|
+
max_tries INT DEFAULT 3,
|
|
169
|
+
available_at VARCHAR(50) NULL,
|
|
170
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
171
|
+
)
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
return `
|
|
175
|
+
CREATE TABLE IF NOT EXISTS _nexa_jobs (
|
|
176
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
177
|
+
name TEXT NOT NULL,
|
|
178
|
+
payload TEXT NOT NULL,
|
|
179
|
+
status TEXT DEFAULT 'pending',
|
|
180
|
+
attempts INTEGER DEFAULT 0,
|
|
181
|
+
max_tries INTEGER DEFAULT 3,
|
|
182
|
+
available_at TEXT,
|
|
183
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
184
|
+
)
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ResourceDefinition } from "../../core/src/resource.js";
|
|
2
|
+
import { buildMySqlColumns, type DatabaseDriver, type DbRow, type QueryOpts } from "./types.js";
|
|
3
|
+
import { buildQuery } from "./query.js";
|
|
4
|
+
import { searchFieldsFor } from "./sqlite.js";
|
|
5
|
+
|
|
6
|
+
export class MysqlDriver implements DatabaseDriver {
|
|
7
|
+
readonly dialect = "mysql" as const;
|
|
8
|
+
private pool: any;
|
|
9
|
+
|
|
10
|
+
constructor(private url: string) {}
|
|
11
|
+
|
|
12
|
+
async connect(): Promise<void> {
|
|
13
|
+
const mysql = await import("mysql2/promise");
|
|
14
|
+
this.pool = mysql.createPool(this.url);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async createTable(resource: ResourceDefinition): Promise<void> {
|
|
18
|
+
const columns = buildMySqlColumns(resource);
|
|
19
|
+
await this.pool.query(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS \`${resource.table}\` (
|
|
21
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
22
|
+
${columns ? `${columns},` : ""}
|
|
23
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
25
|
+
)
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async findAll(table: string, opts: QueryOpts = {}): Promise<DbRow[]> {
|
|
30
|
+
if (!opts.search && !opts.filters && !opts.sort && !opts.softDelete) {
|
|
31
|
+
const limit = opts.limit ?? 100;
|
|
32
|
+
const offset = opts.offset ?? 0;
|
|
33
|
+
const [rows] = await this.pool.query(
|
|
34
|
+
`SELECT * FROM \`${table}\` ORDER BY id DESC LIMIT ? OFFSET ?`,
|
|
35
|
+
[limit, offset]
|
|
36
|
+
);
|
|
37
|
+
return rows as DbRow[];
|
|
38
|
+
}
|
|
39
|
+
const { sql, params } = buildQuery(table, opts);
|
|
40
|
+
const mysqlSql = sql.replace(/FROM (\w+)/, "FROM `$1`");
|
|
41
|
+
const [rows] = await this.pool.query(mysqlSql, params);
|
|
42
|
+
return rows as DbRow[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async count(table: string, opts: QueryOpts = {}): Promise<number> {
|
|
46
|
+
if (!opts.search && !opts.filters && !opts.softDelete) {
|
|
47
|
+
const [rows] = await this.pool.query(`SELECT COUNT(*) as total FROM \`${table}\``);
|
|
48
|
+
return Number((rows as { total: number }[])[0]?.total ?? 0);
|
|
49
|
+
}
|
|
50
|
+
const { countSql, params } = buildQuery(table, { ...opts, limit: 1, offset: 0 });
|
|
51
|
+
const mysqlSql = countSql.replace(/FROM (\w+)/, "FROM `$1`");
|
|
52
|
+
const [rows] = await this.pool.query(mysqlSql, params.slice(0, -2));
|
|
53
|
+
return Number((rows as { total: number }[])[0]?.total ?? 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async findById(table: string, id: number): Promise<DbRow | null> {
|
|
57
|
+
const [rows] = await this.pool.query(`SELECT * FROM \`${table}\` WHERE id = ?`, [id]);
|
|
58
|
+
const list = rows as DbRow[];
|
|
59
|
+
return list[0] ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async insert(table: string, data: Record<string, unknown>): Promise<DbRow> {
|
|
63
|
+
const keys = Object.keys(data);
|
|
64
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
65
|
+
const cols = keys.map((k) => `\`${k}\``).join(", ");
|
|
66
|
+
const [result] = await this.pool.query(
|
|
67
|
+
`INSERT INTO \`${table}\` (${cols}) VALUES (${placeholders})`,
|
|
68
|
+
keys.map((k) => data[k])
|
|
69
|
+
);
|
|
70
|
+
const insertId = (result as { insertId: number }).insertId;
|
|
71
|
+
return (await this.findById(table, insertId))!;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async update(
|
|
75
|
+
table: string,
|
|
76
|
+
id: number,
|
|
77
|
+
data: Record<string, unknown>
|
|
78
|
+
): Promise<DbRow | null> {
|
|
79
|
+
const keys = Object.keys(data);
|
|
80
|
+
if (keys.length === 0) return this.findById(table, id);
|
|
81
|
+
const setClause = keys.map((k) => `\`${k}\` = ?`).join(", ");
|
|
82
|
+
await this.pool.query(`UPDATE \`${table}\` SET ${setClause} WHERE id = ?`, [
|
|
83
|
+
...keys.map((k) => data[k]),
|
|
84
|
+
id,
|
|
85
|
+
]);
|
|
86
|
+
return this.findById(table, id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async delete(table: string, id: number): Promise<boolean> {
|
|
90
|
+
const [result] = await this.pool.query(`DELETE FROM \`${table}\` WHERE id = ?`, [id]);
|
|
91
|
+
return (result as { affectedRows: number }).affectedRows > 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async query(sql: string, params: unknown[] = []): Promise<DbRow[]> {
|
|
95
|
+
const [rows] = await this.pool.query(sql, params);
|
|
96
|
+
return rows as DbRow[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getOne(sql: string, params: unknown[] = []): Promise<DbRow | null> {
|
|
100
|
+
const [rows] = await this.pool.query(sql, params);
|
|
101
|
+
const list = rows as DbRow[];
|
|
102
|
+
return list[0] ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async run(sql: string, params: unknown[] = []): Promise<void> {
|
|
106
|
+
await this.pool.query(sql, params);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
close(): void {
|
|
110
|
+
this.pool?.end?.();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { searchFieldsFor };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ResourceDefinition } from "../../core/src/resource.js";
|
|
2
|
+
import { buildPgColumns, type DatabaseDriver, type DbRow, type QueryOpts } from "./types.js";
|
|
3
|
+
import { buildQuery } from "./query.js";
|
|
4
|
+
import { searchFieldsFor } from "./sqlite.js";
|
|
5
|
+
|
|
6
|
+
function pgPlaceholders(sql: string): string {
|
|
7
|
+
let i = 0;
|
|
8
|
+
return sql.replace(/\?/g, () => `$${++i}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class PostgresDriver implements DatabaseDriver {
|
|
12
|
+
readonly dialect = "postgres" as const;
|
|
13
|
+
private sql: any;
|
|
14
|
+
|
|
15
|
+
constructor(private url: string) {}
|
|
16
|
+
|
|
17
|
+
async connect(): Promise<void> {
|
|
18
|
+
const postgres = (await import("postgres")).default;
|
|
19
|
+
this.sql = postgres(this.url);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async createTable(resource: ResourceDefinition): Promise<void> {
|
|
23
|
+
const columns = buildPgColumns(resource);
|
|
24
|
+
await this.sql.unsafe(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS ${resource.table} (
|
|
26
|
+
id SERIAL PRIMARY KEY,
|
|
27
|
+
${columns ? `${columns},` : ""}
|
|
28
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
29
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
30
|
+
)
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async findAll(table: string, opts: QueryOpts = {}): Promise<DbRow[]> {
|
|
35
|
+
if (!opts.search && !opts.filters && !opts.sort && !opts.softDelete) {
|
|
36
|
+
const limit = opts.limit ?? 100;
|
|
37
|
+
const offset = opts.offset ?? 0;
|
|
38
|
+
return (await this.sql.unsafe(
|
|
39
|
+
`SELECT * FROM ${table} ORDER BY id DESC LIMIT ${limit} OFFSET ${offset}`
|
|
40
|
+
)) as DbRow[];
|
|
41
|
+
}
|
|
42
|
+
const { sql, params } = buildQuery(table, opts);
|
|
43
|
+
return (await this.sql.unsafe(pgPlaceholders(sql), params)) as DbRow[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async count(table: string, opts: QueryOpts = {}): Promise<number> {
|
|
47
|
+
if (!opts.search && !opts.filters && !opts.softDelete) {
|
|
48
|
+
const rows = (await this.sql.unsafe(
|
|
49
|
+
`SELECT COUNT(*)::int as total FROM ${table}`
|
|
50
|
+
)) as { total: number }[];
|
|
51
|
+
return Number(rows[0]?.total ?? 0);
|
|
52
|
+
}
|
|
53
|
+
const { countSql, params } = buildQuery(table, { ...opts, limit: 1, offset: 0 });
|
|
54
|
+
const rows = (await this.sql.unsafe(
|
|
55
|
+
pgPlaceholders(countSql),
|
|
56
|
+
params.slice(0, -2)
|
|
57
|
+
)) as { total: number }[];
|
|
58
|
+
return Number(rows[0]?.total ?? 0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async findById(table: string, id: number): Promise<DbRow | null> {
|
|
62
|
+
const rows = (await this.sql.unsafe(`SELECT * FROM ${table} WHERE id = $1`, [id])) as DbRow[];
|
|
63
|
+
return rows[0] ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async insert(table: string, data: Record<string, unknown>): Promise<DbRow> {
|
|
67
|
+
const keys = Object.keys(data);
|
|
68
|
+
const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
|
|
69
|
+
const rows = (await this.sql.unsafe(
|
|
70
|
+
`INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders}) RETURNING *`,
|
|
71
|
+
keys.map((k) => data[k])
|
|
72
|
+
)) as DbRow[];
|
|
73
|
+
return rows[0];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async update(
|
|
77
|
+
table: string,
|
|
78
|
+
id: number,
|
|
79
|
+
data: Record<string, unknown>
|
|
80
|
+
): Promise<DbRow | null> {
|
|
81
|
+
const keys = Object.keys(data);
|
|
82
|
+
if (keys.length === 0) return this.findById(table, id);
|
|
83
|
+
const setClause = keys.map((k, i) => `${k} = $${i + 1}`).join(", ");
|
|
84
|
+
const params = [...keys.map((k) => data[k]), id];
|
|
85
|
+
await this.sql.unsafe(
|
|
86
|
+
`UPDATE ${table} SET ${setClause}, updated_at = NOW() WHERE id = $${keys.length + 1}`,
|
|
87
|
+
params
|
|
88
|
+
);
|
|
89
|
+
return this.findById(table, id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async delete(table: string, id: number): Promise<boolean> {
|
|
93
|
+
const rows = (await this.sql.unsafe(`DELETE FROM ${table} WHERE id = $1 RETURNING id`, [
|
|
94
|
+
id,
|
|
95
|
+
])) as DbRow[];
|
|
96
|
+
return rows.length > 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async query(sql: string, params: unknown[] = []): Promise<DbRow[]> {
|
|
100
|
+
return (await this.sql.unsafe(pgPlaceholders(sql), params)) as DbRow[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getOne(sql: string, params: unknown[] = []): Promise<DbRow | null> {
|
|
104
|
+
const rows = (await this.sql.unsafe(pgPlaceholders(sql), params)) as DbRow[];
|
|
105
|
+
return rows[0] ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async run(sql: string, params: unknown[] = []): Promise<void> {
|
|
109
|
+
await this.sql.unsafe(pgPlaceholders(sql), params);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
close(): void {
|
|
113
|
+
this.sql?.end?.();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { searchFieldsFor };
|