jsql-neo 4.2.0 → 4.4.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/README.md +112 -94
- package/bin/jsql +138 -0
- package/index.d.ts +351 -0
- package/index.js +18 -0
- package/lib/migrate.js +242 -0
- package/lib/mysql_server.js +335 -6
- package/lib/redis_server.js +448 -0
- package/lib/sql.js +533 -87
- package/lib/table.js +4 -2
- package/lib/web_ui.js +226 -0
- package/package.json +66 -47
- package/test/smoke.js +58 -0
- package/wasm/browser.d.ts +88 -0
- package/wasm/browser.mjs +404 -0
- package/wasm/browser_bg.mjs +462 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSQL-NEO — Rust-powered embedded database (Native / WASM / Pure JS)
|
|
3
|
+
* with a MySQL-compatible server mode.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare namespace JSQLNeo {
|
|
7
|
+
export type FieldType =
|
|
8
|
+
| 'string' | 'text' | 'varchar'
|
|
9
|
+
| 'integer' | 'int' | 'bigint'
|
|
10
|
+
| 'float' | 'double' | 'number'
|
|
11
|
+
| 'boolean'
|
|
12
|
+
| 'date' | 'datetime' | 'timestamp'
|
|
13
|
+
| 'object' | 'json' | 'array'
|
|
14
|
+
| 'any' | 'binary';
|
|
15
|
+
|
|
16
|
+
export interface FieldDef {
|
|
17
|
+
type: FieldType;
|
|
18
|
+
primaryKey?: boolean;
|
|
19
|
+
autoIncrement?: boolean;
|
|
20
|
+
unique?: boolean;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
nullable?: boolean;
|
|
23
|
+
length?: number;
|
|
24
|
+
maxLength?: number;
|
|
25
|
+
min?: number;
|
|
26
|
+
max?: number;
|
|
27
|
+
default?: unknown;
|
|
28
|
+
check?: string;
|
|
29
|
+
ref?: string;
|
|
30
|
+
computed?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Schema = Record<string, FieldDef>;
|
|
34
|
+
|
|
35
|
+
export interface Row {
|
|
36
|
+
id: number | string;
|
|
37
|
+
fields?: Record<string, unknown>;
|
|
38
|
+
created_at?: string;
|
|
39
|
+
updated_at?: string;
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface FindOptions {
|
|
44
|
+
limit?: number;
|
|
45
|
+
offset?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface JSQLOptions {
|
|
49
|
+
dataDir?: string;
|
|
50
|
+
flushThreshold?: number;
|
|
51
|
+
modules?: boolean;
|
|
52
|
+
persistence?: boolean;
|
|
53
|
+
dbName?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type JSQLHook =
|
|
57
|
+
| 'beforeInsert' | 'afterInsert'
|
|
58
|
+
| 'beforeUpdate' | 'afterUpdate'
|
|
59
|
+
| 'beforeDelete' | 'afterDelete'
|
|
60
|
+
| 'beforeFind' | 'afterFind'
|
|
61
|
+
| 'beforeCreateTable' | 'afterCreateTable'
|
|
62
|
+
| 'beforeDropTable' | 'afterDropTable'
|
|
63
|
+
| 'beforeFlush' | 'afterFlush'
|
|
64
|
+
| 'beforeCount' | 'afterCount'
|
|
65
|
+
| 'onStart' | 'onStop';
|
|
66
|
+
|
|
67
|
+
export interface JSQLPlugin {
|
|
68
|
+
name?: string;
|
|
69
|
+
install?(db: JSQL, ctx: PluginContext): void;
|
|
70
|
+
onEvent?(event: string, data: unknown): void;
|
|
71
|
+
hooks?: Partial<Record<JSQLHook, (...args: any[]) => unknown>>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PluginContext {
|
|
75
|
+
name: string;
|
|
76
|
+
engine: JSQL;
|
|
77
|
+
plugin: JSQLPlugin;
|
|
78
|
+
on(hook: JSQLHook, fn: (...args: any[]) => unknown): JSQL;
|
|
79
|
+
onEvent(fn: (event: string, data: unknown) => void): JSQL;
|
|
80
|
+
emit(eventName: string, data: unknown): void;
|
|
81
|
+
tables(): string[];
|
|
82
|
+
hasTable(name: string): boolean;
|
|
83
|
+
getTableSchema(name: string): Schema | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class JSQL {
|
|
87
|
+
constructor(opts?: JSQLOptions);
|
|
88
|
+
use(plugin: JSQLPlugin | ((db: JSQL) => void)): this;
|
|
89
|
+
on(event: JSQLHook, fn: (...args: any[]) => unknown): this;
|
|
90
|
+
onEvent(fn: (event: string, data: unknown) => void): this;
|
|
91
|
+
start(): Promise<void>;
|
|
92
|
+
stop(): Promise<void>;
|
|
93
|
+
flush(): Promise<void>;
|
|
94
|
+
createTable(name: string, schema: Schema): Promise<unknown>;
|
|
95
|
+
dropTable(name: string): Promise<unknown>;
|
|
96
|
+
insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<unknown>;
|
|
97
|
+
insertMany(table: string, data: Record<string, unknown>[]): Promise<unknown>;
|
|
98
|
+
findById(table: string, id: number | string | bigint): Promise<Row | null>;
|
|
99
|
+
findByIds(table: string, ids: Array<number | string>): Promise<Row[] | null>;
|
|
100
|
+
find(table: string, filter?: Record<string, unknown>, opts?: FindOptions): Promise<Row[]>;
|
|
101
|
+
count(table: string): Promise<number>;
|
|
102
|
+
updateById(table: string, id: number | string, data: Record<string, unknown>): Promise<unknown>;
|
|
103
|
+
updateByIds(table: string, entries: Array<[number | string, Record<string, unknown>]>): Promise<unknown>;
|
|
104
|
+
removeById(table: string, id: number | string): Promise<unknown>;
|
|
105
|
+
removeByIds(table: string, ids: Array<number | string>): Promise<unknown>;
|
|
106
|
+
hasTable(name: string): Promise<boolean>;
|
|
107
|
+
getTables(): Promise<string[]>;
|
|
108
|
+
getTableSchema(name: string): Promise<Schema | null>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class NativeJSQL extends JSQL {}
|
|
112
|
+
|
|
113
|
+
export interface MysqlServerOptions {
|
|
114
|
+
port?: number;
|
|
115
|
+
host?: string;
|
|
116
|
+
dataDir?: string;
|
|
117
|
+
defaultDatabase?: string;
|
|
118
|
+
noAuth?: boolean;
|
|
119
|
+
auth?: Record<string, string>;
|
|
120
|
+
allowComments?: boolean;
|
|
121
|
+
safety?: boolean;
|
|
122
|
+
maxConnections?: number;
|
|
123
|
+
handshakeTimeout?: number;
|
|
124
|
+
maxAuthFails?: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class MysqlServer {
|
|
128
|
+
constructor(options?: MysqlServerOptions);
|
|
129
|
+
listen(cb?: (err?: Error) => void): this;
|
|
130
|
+
close(cb?: () => void): void;
|
|
131
|
+
readonly address: { port: number; address: string; family: string } | null;
|
|
132
|
+
listDatabases(): Promise<string[]>;
|
|
133
|
+
createDatabase(name: string, opts?: { ifNotExists?: boolean }): Promise<void>;
|
|
134
|
+
dropDatabase(name: string, opts?: { ifExists?: boolean }): Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createMysqlServer(options?: MysqlServerOptions): MysqlServer;
|
|
138
|
+
|
|
139
|
+
export interface RedisServerOptions {
|
|
140
|
+
port?: number;
|
|
141
|
+
host?: string;
|
|
142
|
+
password?: string | null;
|
|
143
|
+
dataDir?: string | null;
|
|
144
|
+
onQuery?: (cmd: string, args: string[]) => void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class RedisServer {
|
|
148
|
+
constructor(options?: RedisServerOptions);
|
|
149
|
+
listen(): this;
|
|
150
|
+
stop(): void;
|
|
151
|
+
execute(cmd: string, args: string[]): string | number | string[] | null | 'OK' | 'PONG';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createRedisServer(options?: RedisServerOptions): RedisServer;
|
|
155
|
+
|
|
156
|
+
export interface WebUIOptions {
|
|
157
|
+
port?: number;
|
|
158
|
+
host?: string;
|
|
159
|
+
dataDir?: string;
|
|
160
|
+
readonly?: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export class WebUI {
|
|
164
|
+
constructor(options?: WebUIOptions);
|
|
165
|
+
start(): Promise<number>;
|
|
166
|
+
stop(): Promise<void>;
|
|
167
|
+
listDatabases(): { name: string; tables: number }[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface MigrateResult {
|
|
171
|
+
created?: string[];
|
|
172
|
+
inserted?: number;
|
|
173
|
+
errors?: { line?: number; error: string }[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface MigrateTools {
|
|
177
|
+
normalizeSchema(schema: Record<string, unknown>): Record<string, unknown>;
|
|
178
|
+
parseCSV(text: string): string[][];
|
|
179
|
+
toCSV(rows: Record<string, unknown>[], columns?: string[]): string;
|
|
180
|
+
exportTableToJSON(db: unknown, table: string): { schema: unknown; rows: unknown[] };
|
|
181
|
+
exportAllToJSON(db: unknown): Record<string, unknown>;
|
|
182
|
+
importFromJSON(db: unknown, data: Record<string, unknown>): Promise<MigrateResult>;
|
|
183
|
+
exportTableToCSV(db: unknown, table: string): string;
|
|
184
|
+
importFromCSV(db: unknown, table: string, csv: string, opts?: { schema?: unknown }): Promise<MigrateResult>;
|
|
185
|
+
importDump(db: unknown, sql: string, opts?: { strict?: boolean }): Promise<MigrateResult>;
|
|
186
|
+
importDumpFile(db: unknown, file: string, opts?: { strict?: boolean }): Promise<MigrateResult>;
|
|
187
|
+
exportToFile(db: unknown, table: string, outFile: string): Promise<number>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const migrate: MigrateTools;
|
|
191
|
+
export function exportTableToJSON(db: unknown, table: string): { schema: unknown; rows: unknown[] };
|
|
192
|
+
export function exportAllToJSON(db: unknown): Record<string, unknown>;
|
|
193
|
+
export function importFromJSON(db: unknown, data: Record<string, unknown>): Promise<MigrateResult>;
|
|
194
|
+
export function exportTableToCSV(db: unknown, table: string): string;
|
|
195
|
+
export function importFromCSV(db: unknown, table: string, csv: string, opts?: { schema?: unknown }): Promise<MigrateResult>;
|
|
196
|
+
export function importDump(db: unknown, sql: string, opts?: { strict?: boolean }): Promise<MigrateResult>;
|
|
197
|
+
export function importDumpFile(db: unknown, file: string, opts?: { strict?: boolean }): Promise<MigrateResult>;
|
|
198
|
+
export function exportToFile(db: unknown, table: string, outFile: string): Promise<number>;
|
|
199
|
+
|
|
200
|
+
export interface ConnectionOptions {
|
|
201
|
+
host?: string;
|
|
202
|
+
port?: number;
|
|
203
|
+
user?: string;
|
|
204
|
+
password?: string;
|
|
205
|
+
database?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface ConnectionResult {
|
|
209
|
+
insertId: number | null;
|
|
210
|
+
affectedRows: number;
|
|
211
|
+
rows: unknown[];
|
|
212
|
+
fields?: unknown[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export class Connection {
|
|
216
|
+
constructor(options?: ConnectionOptions);
|
|
217
|
+
connect(cb?: (err?: Error) => void): void;
|
|
218
|
+
query(sql: string, params?: unknown[], cb?: (err: Error | null, result: ConnectionResult) => void): void;
|
|
219
|
+
execute(sql: string, params?: unknown[], cb?: (err: Error | null, result: ConnectionResult) => void): void;
|
|
220
|
+
beginTransaction(cb?: (err?: Error) => void): void;
|
|
221
|
+
commit(cb?: (err?: Error) => void): void;
|
|
222
|
+
rollback(cb?: (err?: Error) => void): void;
|
|
223
|
+
end(): void;
|
|
224
|
+
format(sql: string, params?: unknown[]): string;
|
|
225
|
+
escape(value: unknown): string;
|
|
226
|
+
escapeId(value: string): string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class Pool {
|
|
230
|
+
constructor(options?: ConnectionOptions & { max?: number });
|
|
231
|
+
query(sql: string, params?: unknown[], cb?: (err: Error | null, result: ConnectionResult) => void): void;
|
|
232
|
+
getConnection(cb: (err: Error | null, conn: Connection) => void): void;
|
|
233
|
+
end(): void;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function createConnection(options?: ConnectionOptions): Connection;
|
|
237
|
+
export function createPool(options?: ConnectionOptions & { max?: number }): Pool;
|
|
238
|
+
|
|
239
|
+
export interface SQLResult {
|
|
240
|
+
ok: boolean;
|
|
241
|
+
type: string;
|
|
242
|
+
table?: string | null;
|
|
243
|
+
columns?: string[];
|
|
244
|
+
rows?: unknown[][];
|
|
245
|
+
raw?: unknown[];
|
|
246
|
+
affectedRows?: number;
|
|
247
|
+
insertId?: number | null;
|
|
248
|
+
ids?: unknown[];
|
|
249
|
+
error?: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface SQLOptions {
|
|
253
|
+
allowComments?: boolean;
|
|
254
|
+
safety?: boolean;
|
|
255
|
+
maxStatements?: number;
|
|
256
|
+
session?: Record<string, unknown>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function executeSQL(engine: unknown, sql: string, paramsOrOpts?: unknown[] | SQLOptions, opts?: SQLOptions): Promise<SQLResult>;
|
|
260
|
+
export function parseSQL(sql: string): unknown;
|
|
261
|
+
export function splitStatements(sql: string): string[];
|
|
262
|
+
export function applyParams(sql: string, values: unknown[]): string;
|
|
263
|
+
export function escapeValue(value: unknown): string;
|
|
264
|
+
export function escapeId(value: string): string;
|
|
265
|
+
|
|
266
|
+
export interface DatabaseOptions {
|
|
267
|
+
dataDir?: string;
|
|
268
|
+
persist?: boolean;
|
|
269
|
+
inMemoryOnly?: boolean;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export class Database {
|
|
273
|
+
constructor(options?: DatabaseOptions);
|
|
274
|
+
createTable(name: string, schema: Schema): Promise<unknown>;
|
|
275
|
+
insert(table: string, data: Record<string, unknown>): Promise<unknown>;
|
|
276
|
+
find(table: string, filter?: Record<string, unknown>): Promise<Row[]>;
|
|
277
|
+
update(table: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<number>;
|
|
278
|
+
remove(table: string, filter: Record<string, unknown>): Promise<number>;
|
|
279
|
+
findOne(table: string, filter?: Record<string, unknown>): Promise<Row | null>;
|
|
280
|
+
count(table: string): Promise<number>;
|
|
281
|
+
getTables(): Promise<string[]>;
|
|
282
|
+
dropTable(name: string): Promise<void>;
|
|
283
|
+
getTableSchema(name: string): Promise<Schema | null>;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export class Table {
|
|
287
|
+
constructor(name: string, schema: Schema, db?: unknown);
|
|
288
|
+
insert(data: Record<string, unknown>): Promise<unknown>;
|
|
289
|
+
find(filter?: Record<string, unknown>): Promise<Row[]>;
|
|
290
|
+
updateById(id: number | string, data: Record<string, unknown>): Promise<unknown>;
|
|
291
|
+
removeById(id: number | string): Promise<unknown>;
|
|
292
|
+
count(): Promise<number>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export class Query {
|
|
296
|
+
constructor(table: unknown);
|
|
297
|
+
exec(): Promise<Row[]>;
|
|
298
|
+
then(resolve: (rows: Row[]) => unknown, reject?: (err: Error) => unknown): Promise<unknown>;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export class BTree {
|
|
302
|
+
constructor(order?: number);
|
|
303
|
+
insert(key: unknown, value: unknown): void;
|
|
304
|
+
find(key: unknown): unknown;
|
|
305
|
+
range(min: unknown, max: unknown): unknown[];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export class Cache {
|
|
309
|
+
constructor(options?: Record<string, unknown>);
|
|
310
|
+
get(key: string): unknown;
|
|
311
|
+
set(key: string, value: unknown, ttlMs?: number): void;
|
|
312
|
+
del(key: string): void;
|
|
313
|
+
flush(): void;
|
|
314
|
+
close(): void;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export class Plugin {
|
|
318
|
+
static create(plugin: JSQLPlugin): JSQLPlugin;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export class ModuleManager {
|
|
322
|
+
constructor(opts?: { cwd?: string });
|
|
323
|
+
applyTo(engine: JSQL): void;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export class JSQL_Error extends Error {
|
|
327
|
+
code: number;
|
|
328
|
+
codeKey: string;
|
|
329
|
+
args: unknown[];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const ErrorCodes: Record<string, { code: number; msg: string }>;
|
|
333
|
+
|
|
334
|
+
export class JSQLFormat {
|
|
335
|
+
constructor(db: unknown);
|
|
336
|
+
dump(): string;
|
|
337
|
+
load(dump: string): void;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export class Datastore {
|
|
341
|
+
constructor(options?: DatabaseOptions);
|
|
342
|
+
insert(docs: unknown): Promise<unknown>;
|
|
343
|
+
find(query?: unknown): Promise<unknown[]>;
|
|
344
|
+
update(query: unknown, update: unknown): Promise<number>;
|
|
345
|
+
remove(query: unknown): Promise<number>;
|
|
346
|
+
loadDatabase(): Promise<void>;
|
|
347
|
+
persistence: { persistCachedDatabase: (cb?: (err?: Error) => void) => void };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export = JSQLNeo;
|
package/index.js
CHANGED
|
@@ -19,6 +19,9 @@ const sql = require('./lib/sql');
|
|
|
19
19
|
const { Datastore } = require('./lib/nedb_compat');
|
|
20
20
|
const mysqlCompat = require('./lib/mysql_compat');
|
|
21
21
|
const { createMysqlServer, MysqlServer } = require('./lib/mysql_server');
|
|
22
|
+
const migrate = require('./lib/migrate');
|
|
23
|
+
const { WebUI } = require('./lib/web_ui');
|
|
24
|
+
const { RedisServer, createRedisServer } = require('./lib/redis_server');
|
|
22
25
|
|
|
23
26
|
module.exports = {
|
|
24
27
|
JSQL: WasmClient.JSQL,
|
|
@@ -44,4 +47,19 @@ module.exports = {
|
|
|
44
47
|
mysql: mysqlCompat,
|
|
45
48
|
createMysqlServer,
|
|
46
49
|
MysqlServer,
|
|
50
|
+
// 迁移工具: mysqldump 导入 / JSON / CSV
|
|
51
|
+
migrate,
|
|
52
|
+
exportTableToJSON: migrate.exportTableToJSON,
|
|
53
|
+
exportAllToJSON: migrate.exportAllToJSON,
|
|
54
|
+
importFromJSON: migrate.importFromJSON,
|
|
55
|
+
exportTableToCSV: migrate.exportTableToCSV,
|
|
56
|
+
importFromCSV: migrate.importFromCSV,
|
|
57
|
+
importDump: migrate.importDump,
|
|
58
|
+
importDumpFile: migrate.importDumpFile,
|
|
59
|
+
exportToFile: migrate.exportToFile,
|
|
60
|
+
// Web UI
|
|
61
|
+
WebUI,
|
|
62
|
+
// Redis 兼容服务器
|
|
63
|
+
RedisServer,
|
|
64
|
+
createRedisServer,
|
|
47
65
|
};
|
package/lib/migrate.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Migration tools: mysqldump import, JSON import/export, CSV import/export.
|
|
3
|
+
*
|
|
4
|
+
* Works against any engine exposing:
|
|
5
|
+
* hasTable(name) / getTableSchema(name) / find(name, {}, {limit, offset})
|
|
6
|
+
* createTable(name, schema) / insert(name, rows) / executeSQL(sql, ...)
|
|
7
|
+
* (Database instances and the jsql-neo MySQL server engine both qualify.)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { splitStatements, executeSQL } = require('./sql');
|
|
13
|
+
|
|
14
|
+
function normalizeSchema(schema) {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [name, def] of Object.entries(schema || {})) {
|
|
17
|
+
const d = typeof def === 'string' ? { type: def } : { ...def };
|
|
18
|
+
if (!d.type) d.type = typeof d === 'object' ? 'any' : 'string';
|
|
19
|
+
if (d.type === 'int' || d.type === 'bigint' || d.type === 'smallint' || d.type === 'tinyint') d.type = 'integer';
|
|
20
|
+
if (d.type === 'varchar' || d.type === 'text' || d.type === 'char') d.type = 'string';
|
|
21
|
+
if (d.type === 'double' || d.type === 'real' || d.type === 'decimal' || d.type === 'numeric') d.type = 'float';
|
|
22
|
+
if (d.type === 'bool') d.type = 'boolean';
|
|
23
|
+
delete d.length;
|
|
24
|
+
if (d.maxLength) { d.length = d.maxLength; delete d.maxLength; }
|
|
25
|
+
out[name] = d;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function serializeValue(v) {
|
|
31
|
+
if (v === null || v === undefined) return '';
|
|
32
|
+
if (v instanceof Date) return v.toISOString();
|
|
33
|
+
if (typeof v === 'object') return JSON.stringify(v);
|
|
34
|
+
return String(v);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseValue(str, type) {
|
|
38
|
+
if (str === '' || str === null || str === undefined) return null;
|
|
39
|
+
const t = String(type || 'string').toLowerCase();
|
|
40
|
+
if (t === 'integer') return Number.isFinite(Number(str)) ? Math.trunc(Number(str)) : str;
|
|
41
|
+
if (t === 'float' || t === 'number') return Number.isFinite(Number(str)) ? Number(str) : str;
|
|
42
|
+
if (t === 'boolean') {
|
|
43
|
+
const s = str.toLowerCase();
|
|
44
|
+
if (['1', 'true', 'yes', 'y'].includes(s)) return true;
|
|
45
|
+
if (['0', 'false', 'no', 'n'].includes(s)) return false;
|
|
46
|
+
return str;
|
|
47
|
+
}
|
|
48
|
+
if (t === 'object' || t === 'array') {
|
|
49
|
+
try { return JSON.parse(str); } catch (e) { return str; }
|
|
50
|
+
}
|
|
51
|
+
return str;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseCSV(text) {
|
|
55
|
+
const rows = [];
|
|
56
|
+
let row = [];
|
|
57
|
+
let field = '';
|
|
58
|
+
let inQuotes = false;
|
|
59
|
+
let i = 0;
|
|
60
|
+
const n = text.length;
|
|
61
|
+
while (i < n) {
|
|
62
|
+
const c = text[i];
|
|
63
|
+
if (inQuotes) {
|
|
64
|
+
if (c === '"') {
|
|
65
|
+
if (text[i + 1] === '"') { field += '"'; i += 2; continue; }
|
|
66
|
+
inQuotes = false;
|
|
67
|
+
i++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
field += c;
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c === '"' && field === '') { inQuotes = true; i++; continue; }
|
|
75
|
+
if (c === ',') { row.push(field); field = ''; i++; continue; }
|
|
76
|
+
if (c === '\n' || c === '\r') {
|
|
77
|
+
if (c === '\r' && text[i + 1] === '\n') i++;
|
|
78
|
+
row.push(field);
|
|
79
|
+
field = '';
|
|
80
|
+
if (row.length > 1 || row[0] !== '') rows.push(row);
|
|
81
|
+
row = [];
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
field += c;
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
if (field !== '' || row.length > 0) {
|
|
89
|
+
row.push(field);
|
|
90
|
+
if (row.length > 1 || row[0] !== '') rows.push(row);
|
|
91
|
+
}
|
|
92
|
+
return rows;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toCSV(rows, columns) {
|
|
96
|
+
const escape = (v) => {
|
|
97
|
+
const s = serializeValue(v);
|
|
98
|
+
return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
|
99
|
+
};
|
|
100
|
+
const lines = [columns.map(escape).join(',')];
|
|
101
|
+
for (const r of rows) {
|
|
102
|
+
lines.push(columns.map(c => escape(r[c])).join(','));
|
|
103
|
+
}
|
|
104
|
+
return lines.join('\n') + '\n';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ---------- JSON ---------- */
|
|
108
|
+
|
|
109
|
+
async function exportTableToJSON(engine, table) {
|
|
110
|
+
const schema = await engine.getTableSchema(table);
|
|
111
|
+
if (!schema) throw new Error(`Table '${table}' does not exist`);
|
|
112
|
+
const rows = await engine.find(table, {}, { limit: 1e9, offset: 0 });
|
|
113
|
+
return { table, schema, rows };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function exportAllToJSON(engine, tables) {
|
|
117
|
+
const list = tables || await engine.getTables();
|
|
118
|
+
const out = {};
|
|
119
|
+
for (const t of list) out[t] = await exportTableToJSON(engine, t);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function importFromJSON(engine, data) {
|
|
124
|
+
const tables = typeof data === 'string' ? JSON.parse(data) : data;
|
|
125
|
+
const created = [];
|
|
126
|
+
let inserted = 0;
|
|
127
|
+
for (const [name, t] of Object.entries(tables)) {
|
|
128
|
+
if (!t || !t.schema) continue;
|
|
129
|
+
if (engine.hasTable(name)) await engine.dropTable(name);
|
|
130
|
+
await engine.createTable(name, normalizeSchema(t.schema));
|
|
131
|
+
created.push(name);
|
|
132
|
+
if (Array.isArray(t.rows) && t.rows.length > 0) {
|
|
133
|
+
const ids = await engine.insert(name, t.rows.map(r => ({ ...r.fields, id: r.id })));
|
|
134
|
+
inserted += Array.isArray(ids) ? ids.length : t.rows.length;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { created, inserted };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ---------- CSV ---------- */
|
|
141
|
+
|
|
142
|
+
async function exportTableToCSV(engine, table) {
|
|
143
|
+
const schema = await engine.getTableSchema(table);
|
|
144
|
+
if (!schema) throw new Error(`Table '${table}' does not exist`);
|
|
145
|
+
const columns = Object.keys(schema);
|
|
146
|
+
const rows = await engine.find(table, {}, { limit: 1e9, offset: 0 });
|
|
147
|
+
return toCSV(rows, columns);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function importFromCSV(engine, table, csv, opts = {}) {
|
|
151
|
+
const schema = opts.schema || await engine.getTableSchema(table);
|
|
152
|
+
const rows = parseCSV(csv);
|
|
153
|
+
if (rows.length === 0) return { inserted: 0 };
|
|
154
|
+
let columns;
|
|
155
|
+
let start = 0;
|
|
156
|
+
if (opts.header !== false) {
|
|
157
|
+
columns = rows[0];
|
|
158
|
+
start = 1;
|
|
159
|
+
} else if (schema) {
|
|
160
|
+
columns = Object.keys(schema);
|
|
161
|
+
} else {
|
|
162
|
+
columns = rows[0].map((_, i) => 'col' + (i + 1));
|
|
163
|
+
}
|
|
164
|
+
if (!engine.hasTable(table)) {
|
|
165
|
+
if (!schema) {
|
|
166
|
+
throw new Error(`Table '${table}' does not exist; provide opts.schema to create it`);
|
|
167
|
+
}
|
|
168
|
+
await engine.createTable(table, normalizeSchema(schema));
|
|
169
|
+
}
|
|
170
|
+
const dataRows = [];
|
|
171
|
+
for (let i = start; i < rows.length; i++) {
|
|
172
|
+
const row = {};
|
|
173
|
+
for (let j = 0; j < columns.length; j++) {
|
|
174
|
+
const type = schema && schema[columns[j]] ? schema[columns[j]].type : 'string';
|
|
175
|
+
row[columns[j]] = parseValue(rows[i][j], type);
|
|
176
|
+
}
|
|
177
|
+
if (row.id === null || row.id === undefined || row.id === '') delete row.id;
|
|
178
|
+
dataRows.push(row);
|
|
179
|
+
}
|
|
180
|
+
const ids = dataRows.length > 0 ? await engine.insert(table, dataRows) : [];
|
|
181
|
+
return { inserted: dataRows.length, ids };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ---------- mysqldump ---------- */
|
|
185
|
+
|
|
186
|
+
async function importDump(engine, sqlText, opts = {}) {
|
|
187
|
+
const statements = splitStatements(sqlText);
|
|
188
|
+
const created = [];
|
|
189
|
+
let inserted = 0;
|
|
190
|
+
const errors = [];
|
|
191
|
+
for (const raw of statements) {
|
|
192
|
+
const stmt = raw.trim();
|
|
193
|
+
if (!stmt) continue;
|
|
194
|
+
if (stmt.startsWith('--') || stmt.startsWith('#')) continue;
|
|
195
|
+
const upper = stmt.toUpperCase();
|
|
196
|
+
if (upper.startsWith('LOCK ') || upper.startsWith('UNLOCK ')) continue;
|
|
197
|
+
if (upper.startsWith('/*!')) continue;
|
|
198
|
+
if (upper.startsWith('SET ') && opts.skipSet !== false) continue;
|
|
199
|
+
try {
|
|
200
|
+
const r = await executeSQL(engine, stmt, { safety: false });
|
|
201
|
+
if (r && r.type === 'createTable') created.push(r.table);
|
|
202
|
+
if (r && r.type === 'insert') inserted += (r.ids || []).length || r.affectedRows || 0;
|
|
203
|
+
} catch (e) {
|
|
204
|
+
if (opts.strict) throw e;
|
|
205
|
+
errors.push({ sql: stmt.slice(0, 120), error: e.message });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { created, inserted, errors };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function importDumpFile(engine, filePath, opts = {}) {
|
|
212
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
213
|
+
return importDump(engine, text, opts);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function exportToFile(engine, table, filePath) {
|
|
217
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
218
|
+
let content;
|
|
219
|
+
if (ext === '.json') {
|
|
220
|
+
content = JSON.stringify(await exportTableToJSON(engine, table), null, 2);
|
|
221
|
+
} else if (ext === '.csv') {
|
|
222
|
+
content = await exportTableToCSV(engine, table);
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error('Unsupported export format (use .json or .csv): ' + filePath);
|
|
225
|
+
}
|
|
226
|
+
fs.writeFileSync(filePath, content);
|
|
227
|
+
return content.length;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
normalizeSchema,
|
|
232
|
+
parseCSV,
|
|
233
|
+
toCSV,
|
|
234
|
+
exportTableToJSON,
|
|
235
|
+
exportAllToJSON,
|
|
236
|
+
importFromJSON,
|
|
237
|
+
exportTableToCSV,
|
|
238
|
+
importFromCSV,
|
|
239
|
+
importDump,
|
|
240
|
+
importDumpFile,
|
|
241
|
+
exportToFile,
|
|
242
|
+
};
|