stabilize-orm 1.0.6 → 1.0.7
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 +1 -1
- package/dist/stabilize-cli.exe +0 -0
- package/package.json +25 -12
- package/.eslintrc.json +0 -10
- package/.github/workflows/ci-cd.yml +0 -73
- package/bun.lock +0 -605
- package/cli/stabilize-cli.ts +0 -401
- package/examples/config/dataase.ts +0 -8
- package/examples/migrations/20251013_seed_history.ts +0 -0
- package/examples/models/Role.ts +0 -11
- package/examples/models/User.ts +0 -22
- package/examples/seeds/20251013_additional_seed.ts +0 -29
- package/examples/seeds/20251013_initial_seed.ts +0 -33
- package/src/LICENSE +0 -21
- package/src/cache.ts +0 -110
- package/src/client.ts +0 -258
- package/src/decorators.ts +0 -99
- package/src/index.ts +0 -143
- package/src/logger.ts +0 -126
- package/src/migrations.ts +0 -81
- package/src/query-builder.ts +0 -96
- package/src/repository.ts +0 -565
- package/src/types.ts +0 -76
- package/tests/migrations.test.ts +0 -141
- package/tsconfig.json +0 -32
package/cli/stabilize-cli.ts
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { program } from "commander";
|
|
3
|
-
import {
|
|
4
|
-
generateMigration,
|
|
5
|
-
Stabilize,
|
|
6
|
-
DBType,
|
|
7
|
-
type DBConfig,
|
|
8
|
-
LogLevel,
|
|
9
|
-
type LoggerConfig,
|
|
10
|
-
} from "../src";
|
|
11
|
-
import * as fs from "fs/promises";
|
|
12
|
-
import * as path from "path";
|
|
13
|
-
import { glob } from "glob";
|
|
14
|
-
|
|
15
|
-
program.version("1.0.5").description("Stabilize ORM CLI");
|
|
16
|
-
|
|
17
|
-
program
|
|
18
|
-
.command("generate <type> <name>")
|
|
19
|
-
.description("Generate a model, migration, or seed")
|
|
20
|
-
.option(
|
|
21
|
-
"-l, --log-level <level>",
|
|
22
|
-
"Log level (error, warn, info, debug)",
|
|
23
|
-
"info",
|
|
24
|
-
)
|
|
25
|
-
.action(async (type: string, name: string, options) => {
|
|
26
|
-
const loggerConfig: LoggerConfig = {
|
|
27
|
-
level: options.logLevel as LogLevel,
|
|
28
|
-
filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
|
|
29
|
-
maxFileSize: 5 * 1024 * 1024,
|
|
30
|
-
maxFiles: 3,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
if (type === "migration") {
|
|
34
|
-
const modelPath = path.resolve(process.cwd(), "models", `${name}.ts`);
|
|
35
|
-
try {
|
|
36
|
-
const modelModule = await import(modelPath);
|
|
37
|
-
const model = Object.values(modelModule)[0] as new (
|
|
38
|
-
...args: any[]
|
|
39
|
-
) => any;
|
|
40
|
-
const migration = await generateMigration(
|
|
41
|
-
model,
|
|
42
|
-
`create_${name.toLowerCase()}`,
|
|
43
|
-
);
|
|
44
|
-
const migrationDir = path.resolve(process.cwd(), "migrations");
|
|
45
|
-
await fs.mkdir(migrationDir, { recursive: true });
|
|
46
|
-
const timestamp = new Date().toISOString().replace(/[-:T.]/g, "");
|
|
47
|
-
const migrationFile = path.join(
|
|
48
|
-
migrationDir,
|
|
49
|
-
`${timestamp}_${name.toLowerCase()}.ts`,
|
|
50
|
-
);
|
|
51
|
-
await fs.writeFile(
|
|
52
|
-
migrationFile,
|
|
53
|
-
`export default ${JSON.stringify(migration, null, 2)};`,
|
|
54
|
-
);
|
|
55
|
-
console.log(`Migration generated: ${migrationFile}`);
|
|
56
|
-
} catch (error) {
|
|
57
|
-
console.error("Error generating migration:", error);
|
|
58
|
-
}
|
|
59
|
-
} else if (type === "model") {
|
|
60
|
-
const modelDir = path.resolve(process.cwd(), "models");
|
|
61
|
-
await fs.mkdir(modelDir, { recursive: true });
|
|
62
|
-
const modelFile = path.join(modelDir, `${name}.ts`);
|
|
63
|
-
const modelContent = `
|
|
64
|
-
import { Model, Column, Required } from 'stabilize-orm';
|
|
65
|
-
|
|
66
|
-
@Model('${name.toLowerCase()}s')
|
|
67
|
-
export class ${name} {
|
|
68
|
-
@Column('id', 'INTEGER')
|
|
69
|
-
id?: number;
|
|
70
|
-
|
|
71
|
-
@Column('name', 'TEXT')
|
|
72
|
-
@Required()
|
|
73
|
-
name: string;
|
|
74
|
-
}
|
|
75
|
-
`;
|
|
76
|
-
await fs.writeFile(modelFile, modelContent);
|
|
77
|
-
console.log(`Model generated: ${modelFile}`);
|
|
78
|
-
} else if (type === "seed") {
|
|
79
|
-
const seedDir = path.resolve(process.cwd(), "seeds");
|
|
80
|
-
await fs.mkdir(seedDir, { recursive: true });
|
|
81
|
-
const timestamp = new Date().toISOString().replace(/[-:T.]/g, "");
|
|
82
|
-
const seedFile = path.join(
|
|
83
|
-
seedDir,
|
|
84
|
-
`${timestamp}_${name.toLowerCase()}.ts`,
|
|
85
|
-
);
|
|
86
|
-
const seedContent = `
|
|
87
|
-
import { Stabilize } from 'stabilize-orm';
|
|
88
|
-
|
|
89
|
-
export const dependencies = [];
|
|
90
|
-
|
|
91
|
-
export async function seed(orm: Stabilize) {
|
|
92
|
-
const repo = orm.getRepository(${name});
|
|
93
|
-
await repo.bulkCreate([
|
|
94
|
-
{ name: '${name} 1' },
|
|
95
|
-
{ name: '${name} 2' },
|
|
96
|
-
], { batchSize: 100 });
|
|
97
|
-
|
|
98
|
-
await orm['client'].query(
|
|
99
|
-
\`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)\`,
|
|
100
|
-
['${timestamp}_${name.toLowerCase()}', new Date().toISOString()]
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export async function rollback(orm: Stabilize) {
|
|
105
|
-
const repo = orm.getRepository(${name});
|
|
106
|
-
const entities = await repo.find().execute(orm['client']);
|
|
107
|
-
await repo.bulkDelete(entities.map(e => e.id!), { batchSize: 100 });
|
|
108
|
-
|
|
109
|
-
await orm['client'].query(
|
|
110
|
-
\`DELETE FROM seed_history WHERE name = ?\`,
|
|
111
|
-
['${timestamp}_${name.toLowerCase()}']
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
`;
|
|
115
|
-
await fs.writeFile(seedFile, seedContent);
|
|
116
|
-
console.log(`Seed generated: ${seedFile}`);
|
|
117
|
-
} else {
|
|
118
|
-
console.error('Invalid type. Use "model", "migration", or "seed".');
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
program
|
|
123
|
-
.command("seed")
|
|
124
|
-
.description("Run seed files to populate the database")
|
|
125
|
-
.option(
|
|
126
|
-
"-c, --config <path>",
|
|
127
|
-
"Path to database config file",
|
|
128
|
-
"config/database.ts",
|
|
129
|
-
)
|
|
130
|
-
.option(
|
|
131
|
-
"-l, --log-level <level>",
|
|
132
|
-
"Log level (error, warn, info, debug)",
|
|
133
|
-
"info",
|
|
134
|
-
)
|
|
135
|
-
.action(async (options) => {
|
|
136
|
-
try {
|
|
137
|
-
const configPath = path.resolve(process.cwd(), options.config);
|
|
138
|
-
const configModule = await import(configPath);
|
|
139
|
-
const config: DBConfig = configModule.default || configModule;
|
|
140
|
-
|
|
141
|
-
const loggerConfig: LoggerConfig = {
|
|
142
|
-
level: options.logLevel as LogLevel,
|
|
143
|
-
filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
|
|
144
|
-
maxFileSize: 5 * 1024 * 1024,
|
|
145
|
-
maxFiles: 3,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const orm = new Stabilize(
|
|
149
|
-
config,
|
|
150
|
-
{ enabled: false, ttl: 60 },
|
|
151
|
-
loggerConfig,
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
await orm["client"].query(`
|
|
155
|
-
CREATE TABLE IF NOT EXISTS seed_history (
|
|
156
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
157
|
-
name TEXT NOT NULL,
|
|
158
|
-
applied_at TEXT NOT NULL
|
|
159
|
-
)
|
|
160
|
-
`);
|
|
161
|
-
|
|
162
|
-
const seedDir = path.resolve(process.cwd(), "seeds");
|
|
163
|
-
const seedFiles = await glob(`${seedDir}/*.ts`);
|
|
164
|
-
if (seedFiles.length === 0) {
|
|
165
|
-
console.log("No seed files found in seeds/ directory.");
|
|
166
|
-
await orm.close();
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const seedGraph = new Map<
|
|
171
|
-
string,
|
|
172
|
-
{ file: string; dependencies: string[] }
|
|
173
|
-
>();
|
|
174
|
-
for (const file of seedFiles) {
|
|
175
|
-
const seedName = path.basename(file, ".ts");
|
|
176
|
-
const seedModule = await import(file);
|
|
177
|
-
seedGraph.set(seedName, {
|
|
178
|
-
file,
|
|
179
|
-
dependencies: seedModule.dependencies || [],
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const orderedSeeds = topologicalSort(seedGraph);
|
|
184
|
-
|
|
185
|
-
console.log(`Running ${orderedSeeds.length} seed files...`);
|
|
186
|
-
for (const seedName of orderedSeeds) {
|
|
187
|
-
const { file } = seedGraph.get(seedName)!;
|
|
188
|
-
const applied = await orm["client"].query<{ id: number }>(
|
|
189
|
-
`SELECT id FROM seed_history WHERE name = ?`,
|
|
190
|
-
[seedName],
|
|
191
|
-
);
|
|
192
|
-
if (applied.length > 0) {
|
|
193
|
-
console.log(`Skipping already applied seed: ${seedName}`);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
console.log(`Executing seed: ${seedName}`);
|
|
198
|
-
const seedModule = await import(file);
|
|
199
|
-
const seedFn = seedModule.default || seedModule.seed;
|
|
200
|
-
if (typeof seedFn === "function") {
|
|
201
|
-
await seedFn(orm);
|
|
202
|
-
} else {
|
|
203
|
-
console.error(
|
|
204
|
-
`Seed file ${file} must export a default function or a function named 'seed'.`,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
console.log("Seeding completed successfully.");
|
|
210
|
-
await orm.close();
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.error("Seeding failed:", error);
|
|
213
|
-
process.exit(1);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
program
|
|
218
|
-
.command("seed:rollback")
|
|
219
|
-
.description("Rollback the most recently applied seed")
|
|
220
|
-
.option(
|
|
221
|
-
"-c, --config <path>",
|
|
222
|
-
"Path to database config file",
|
|
223
|
-
"config/database.ts",
|
|
224
|
-
)
|
|
225
|
-
.option(
|
|
226
|
-
"-l, --log-level <level>",
|
|
227
|
-
"Log level (error, warn, info, debug)",
|
|
228
|
-
"info",
|
|
229
|
-
)
|
|
230
|
-
.action(async (options) => {
|
|
231
|
-
try {
|
|
232
|
-
const configPath = path.resolve(process.cwd(), options.config);
|
|
233
|
-
const configModule = await import(configPath);
|
|
234
|
-
const config: DBConfig = configModule.default || configModule;
|
|
235
|
-
|
|
236
|
-
const loggerConfig: LoggerConfig = {
|
|
237
|
-
level: options.logLevel as LogLevel,
|
|
238
|
-
filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
|
|
239
|
-
maxFileSize: 5 * 1024 * 1024,
|
|
240
|
-
maxFiles: 3,
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const orm = new Stabilize(
|
|
244
|
-
config,
|
|
245
|
-
{ enabled: false, ttl: 60 },
|
|
246
|
-
loggerConfig,
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
const seedDir = path.resolve(process.cwd(), "seeds");
|
|
250
|
-
const seedFiles = await glob(`${seedDir}/*.ts`);
|
|
251
|
-
const seedGraph = new Map<
|
|
252
|
-
string,
|
|
253
|
-
{ file: string; dependencies: string[] }
|
|
254
|
-
>();
|
|
255
|
-
for (const file of seedFiles) {
|
|
256
|
-
const seedName = path.basename(file, ".ts");
|
|
257
|
-
const seedModule = await import(file);
|
|
258
|
-
seedGraph.set(seedName, {
|
|
259
|
-
file,
|
|
260
|
-
dependencies: seedModule.dependencies || [],
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const latestSeed = await orm["client"].query<{ name: string }>(
|
|
265
|
-
`SELECT name FROM seed_history ORDER BY applied_at DESC LIMIT 1`,
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
if (latestSeed.length === 0) {
|
|
269
|
-
console.log("No seeds to rollback.");
|
|
270
|
-
await orm.close();
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const seedName = latestSeed[0]!.name;
|
|
275
|
-
const seedFile = seedGraph.get(seedName)?.file;
|
|
276
|
-
if (!seedFile) {
|
|
277
|
-
console.error(`Seed file for ${seedName} not found.`);
|
|
278
|
-
await orm.close();
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
console.log(`Rolling back seed: ${seedName}`);
|
|
283
|
-
const seedModule = await import(seedFile);
|
|
284
|
-
const rollbackFn = seedModule.rollback;
|
|
285
|
-
if (typeof rollbackFn === "function") {
|
|
286
|
-
await rollbackFn(orm);
|
|
287
|
-
console.log(`Rolled back seed: ${seedName}`);
|
|
288
|
-
} else {
|
|
289
|
-
console.error(
|
|
290
|
-
`Seed file ${seedFile} must export a 'rollback' function.`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
await orm.close();
|
|
295
|
-
} catch (error) {
|
|
296
|
-
console.error("Seed rollback failed:", error);
|
|
297
|
-
process.exit(1);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
program
|
|
302
|
-
.command("status")
|
|
303
|
-
.description("Display status of migrations and seeds")
|
|
304
|
-
.option(
|
|
305
|
-
"-c, --config <path>",
|
|
306
|
-
"Path to database config file",
|
|
307
|
-
"config/database.ts",
|
|
308
|
-
)
|
|
309
|
-
.option(
|
|
310
|
-
"-l, --log-level <level>",
|
|
311
|
-
"Log level (error, warn, info, debug)",
|
|
312
|
-
"info",
|
|
313
|
-
)
|
|
314
|
-
.action(async (options) => {
|
|
315
|
-
try {
|
|
316
|
-
const configPath = path.resolve(process.cwd(), options.config);
|
|
317
|
-
const configModule = await import(configPath);
|
|
318
|
-
const config: DBConfig = configModule.default || configModule;
|
|
319
|
-
|
|
320
|
-
const loggerConfig: LoggerConfig = {
|
|
321
|
-
level: options.logLevel as LogLevel,
|
|
322
|
-
filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
|
|
323
|
-
maxFileSize: 1 * 1024 * 1024,
|
|
324
|
-
maxFiles: 3,
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const orm = new Stabilize(
|
|
328
|
-
config,
|
|
329
|
-
{ enabled: false, ttl: 60 },
|
|
330
|
-
loggerConfig,
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
const migrationDir = path.resolve(process.cwd(), "migrations");
|
|
334
|
-
const migrationFiles = await glob(`${migrationDir}/*.ts`);
|
|
335
|
-
console.log("\nMigration Status:");
|
|
336
|
-
console.log("-----------------");
|
|
337
|
-
for (const file of migrationFiles.sort()) {
|
|
338
|
-
const migrationName = path.basename(file, ".ts");
|
|
339
|
-
const applied = await orm["client"].query(
|
|
340
|
-
`SELECT name FROM migrations WHERE name = ?`,
|
|
341
|
-
[migrationName],
|
|
342
|
-
);
|
|
343
|
-
console.log(
|
|
344
|
-
`${migrationName}: ${applied.length > 0 ? "Applied" : "Pending"}`,
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const seedDir = path.resolve(process.cwd(), "seeds");
|
|
349
|
-
const seedFiles = await glob(`${seedDir}/*.ts`);
|
|
350
|
-
console.log("\nSeed Status:");
|
|
351
|
-
console.log("-------------");
|
|
352
|
-
for (const file of seedFiles.sort()) {
|
|
353
|
-
const seedName = path.basename(file, ".ts");
|
|
354
|
-
const applied = await orm["client"].query(
|
|
355
|
-
`SELECT name FROM seed_history WHERE name = ?`,
|
|
356
|
-
[seedName],
|
|
357
|
-
);
|
|
358
|
-
console.log(
|
|
359
|
-
`${seedName}: ${applied.length > 0 ? "Applied" : "Pending"}`,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
await orm.close();
|
|
364
|
-
} catch (error) {
|
|
365
|
-
console.error("Status check failed:", error);
|
|
366
|
-
process.exit(1);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
function topologicalSort(
|
|
371
|
-
graph: Map<string, { file: string; dependencies: string[] }>,
|
|
372
|
-
): string[] {
|
|
373
|
-
const result: string[] = [];
|
|
374
|
-
const visited = new Set<string>();
|
|
375
|
-
const temp = new Set<string>();
|
|
376
|
-
|
|
377
|
-
function visit(node: string) {
|
|
378
|
-
if (temp.has(node))
|
|
379
|
-
throw new Error(`Circular dependency detected at ${node}`);
|
|
380
|
-
if (!visited.has(node)) {
|
|
381
|
-
temp.add(node);
|
|
382
|
-
const { dependencies } = graph.get(node)!;
|
|
383
|
-
for (const dep of dependencies) {
|
|
384
|
-
if (!graph.has(dep))
|
|
385
|
-
throw new Error(`Dependency ${dep} not found for ${node}`);
|
|
386
|
-
visit(dep);
|
|
387
|
-
}
|
|
388
|
-
temp.delete(node);
|
|
389
|
-
visited.add(node);
|
|
390
|
-
result.push(node);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
for (const node of graph.keys()) {
|
|
395
|
-
if (!visited.has(node)) visit(node);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return result;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
program.parse(process.argv);
|
|
File without changes
|
package/examples/models/Role.ts
DELETED
package/examples/models/User.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { Model, Column, Required, SoftDelete } from "../../src";
|
|
2
|
-
|
|
3
|
-
@Model("users")
|
|
4
|
-
export class User {
|
|
5
|
-
@Column("id", "INTEGER")
|
|
6
|
-
id?: number;
|
|
7
|
-
|
|
8
|
-
@Column("name", "TEXT")
|
|
9
|
-
@Required()
|
|
10
|
-
name?: string;
|
|
11
|
-
|
|
12
|
-
@Column("email", "TEXT")
|
|
13
|
-
@Required()
|
|
14
|
-
email?: string;
|
|
15
|
-
|
|
16
|
-
@Column("active", "BOOLEAN")
|
|
17
|
-
active?: boolean;
|
|
18
|
-
|
|
19
|
-
@Column("deletedAt", "TEXT")
|
|
20
|
-
@SoftDelete()
|
|
21
|
-
deletedAt?: string;
|
|
22
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Stabilize } from "../../src";
|
|
2
|
-
import { Role } from "../models/Role";
|
|
3
|
-
|
|
4
|
-
export const dependencies = ["20251013_initial_seed"];
|
|
5
|
-
|
|
6
|
-
export async function seed(orm: Stabilize) {
|
|
7
|
-
const repo = orm.getRepository(Role);
|
|
8
|
-
await repo.bulkCreate([{ name: "Admin" }, { name: "User" }], {
|
|
9
|
-
batchSize: 100,
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
await orm["client"].query(
|
|
13
|
-
`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)`,
|
|
14
|
-
["20251013_additional_seed", new Date().toISOString()],
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function rollback(orm: Stabilize) {
|
|
19
|
-
const repo = orm.getRepository(Role);
|
|
20
|
-
const entities = await repo.find().execute(orm["client"]);
|
|
21
|
-
await repo.bulkDelete(
|
|
22
|
-
entities.map((e) => e.id!),
|
|
23
|
-
{ batchSize: 100 },
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
await orm["client"].query(`DELETE FROM seed_history WHERE name = ?`, [
|
|
27
|
-
"20251013_additional_seed",
|
|
28
|
-
]);
|
|
29
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Stabilize } from "../../src";
|
|
2
|
-
import { User } from "../models/User";
|
|
3
|
-
|
|
4
|
-
export const dependencies = [];
|
|
5
|
-
|
|
6
|
-
export async function seed(orm: Stabilize) {
|
|
7
|
-
const repo = orm.getRepository(User);
|
|
8
|
-
await repo.bulkCreate(
|
|
9
|
-
[
|
|
10
|
-
{ name: "Alice", email: "alice@example.com", active: true },
|
|
11
|
-
{ name: "Bob", email: "bob@example.com", active: true },
|
|
12
|
-
],
|
|
13
|
-
{ batchSize: 100 },
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
await orm["client"].query(
|
|
17
|
-
`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)`,
|
|
18
|
-
["20251013_initial_seed", new Date().toISOString()],
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function rollback(orm: Stabilize) {
|
|
23
|
-
const repo = orm.getRepository(User);
|
|
24
|
-
const entities = await repo.find().execute(orm["client"]);
|
|
25
|
-
await repo.bulkDelete(
|
|
26
|
-
entities.map((e) => e.id!),
|
|
27
|
-
{ batchSize: 100 },
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
await orm["client"].query(`DELETE FROM seed_history WHERE name = ?`, [
|
|
31
|
-
"20251013_initial_seed",
|
|
32
|
-
]);
|
|
33
|
-
}
|
package/src/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 ElectronSz
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/src/cache.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import Redis from "ioredis";
|
|
2
|
-
import { type CacheConfig, type CacheStats } from "./types";
|
|
3
|
-
import { ConsoleLogger, type Logger } from "./logger";
|
|
4
|
-
|
|
5
|
-
export class Cache {
|
|
6
|
-
private redis: Redis | null = null;
|
|
7
|
-
private ttl: number;
|
|
8
|
-
private prefix: string;
|
|
9
|
-
private strategy: "cache-aside" | "write-through";
|
|
10
|
-
private logger: Logger;
|
|
11
|
-
private hits: number = 0;
|
|
12
|
-
private misses: number = 0;
|
|
13
|
-
|
|
14
|
-
constructor(config: CacheConfig, logger: Logger = new ConsoleLogger()) {
|
|
15
|
-
this.ttl = config.ttl;
|
|
16
|
-
this.prefix = config.cachePrefix || "cache:";
|
|
17
|
-
this.strategy = config.strategy || "cache-aside";
|
|
18
|
-
this.logger = logger;
|
|
19
|
-
|
|
20
|
-
if (config.enabled && config.redisUrl) {
|
|
21
|
-
this.redis = new Redis(config.redisUrl, { lazyConnect: true });
|
|
22
|
-
this.redis.on("error", (error) => this.logger.logError(error));
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
getStrategy() {
|
|
27
|
-
return this.strategy;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async get<T>(key: string): Promise<T | null> {
|
|
31
|
-
if (!this.redis) return null;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const data = await this.redis.get(this.prefix + key);
|
|
35
|
-
if (data) {
|
|
36
|
-
this.hits++;
|
|
37
|
-
this.logger.logDebug(`Cache hit for key: ${key}`);
|
|
38
|
-
return JSON.parse(data) as T;
|
|
39
|
-
}
|
|
40
|
-
this.misses++;
|
|
41
|
-
this.logger.logDebug(`Cache miss for key: ${key}`);
|
|
42
|
-
return null;
|
|
43
|
-
} catch (error) {
|
|
44
|
-
this.logger.logError(error as Error);
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async set<T>(key: string, value: T, ttl: number = this.ttl): Promise<void> {
|
|
50
|
-
if (!this.redis) return;
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
await this.redis.set(this.prefix + key, JSON.stringify(value), "EX", ttl);
|
|
54
|
-
this.logger.logDebug(`Cache set for key: ${key}`);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
this.logger.logError(error as Error);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async invalidate(keys: string[]): Promise<void> {
|
|
61
|
-
if (!this.redis) return;
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const pipeline = this.redis.pipeline();
|
|
65
|
-
for (const key of keys) {
|
|
66
|
-
pipeline.del(this.prefix + key);
|
|
67
|
-
this.logger.logDebug(`Cache invalidated for key: ${key}`);
|
|
68
|
-
}
|
|
69
|
-
await pipeline.exec();
|
|
70
|
-
} catch (error) {
|
|
71
|
-
this.logger.logError(error as Error);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async invalidatePattern(pattern: string): Promise<void> {
|
|
76
|
-
if (!this.redis) return;
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const keys = await this.redis.keys(this.prefix + pattern);
|
|
80
|
-
if (keys.length > 0) {
|
|
81
|
-
const pipeline = this.redis.pipeline();
|
|
82
|
-
for (const key of keys) {
|
|
83
|
-
pipeline.del(key);
|
|
84
|
-
this.logger.logDebug(`Cache invalidated for pattern: ${pattern}`);
|
|
85
|
-
}
|
|
86
|
-
await pipeline.exec();
|
|
87
|
-
}
|
|
88
|
-
} catch (error) {
|
|
89
|
-
this.logger.logError(error as Error);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async getStats(): Promise<CacheStats> {
|
|
94
|
-
if (!this.redis) return { hits: 0, misses: 0, keys: 0 };
|
|
95
|
-
try {
|
|
96
|
-
const keys = await this.redis.keys(this.prefix + "*");
|
|
97
|
-
return { hits: this.hits, misses: this.misses, keys: keys.length };
|
|
98
|
-
} catch (error) {
|
|
99
|
-
this.logger.logError(error as Error);
|
|
100
|
-
return { hits: this.hits, misses: this.misses, keys: 0 };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async disconnect(): Promise<void> {
|
|
105
|
-
if (this.redis) {
|
|
106
|
-
await this.redis.quit();
|
|
107
|
-
this.logger.logInfo("Redis connection closed");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|