@vincent99/vlib 0.1.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/LICENSE +178 -0
- package/README.md +107 -0
- package/bin/vlib.js +10 -0
- package/dist/AdminForm.vue_vue_type_style_index_0_lang-xCk1ywLq.js +753 -0
- package/dist/auth/middleware.d.ts +18 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +44 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/password.d.ts +10 -0
- package/dist/auth/password.d.ts.map +1 -0
- package/dist/auth/password.js +44 -0
- package/dist/auth/password.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/AdminForm.vue.d.ts +7 -0
- package/dist/components/AdminTable.vue.d.ts +5 -0
- package/dist/components/AppLayout.vue.d.ts +36 -0
- package/dist/components/NavSidebar.vue.d.ts +11 -0
- package/dist/components/TableView.vue.d.ts +52 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +8 -0
- package/dist/components/types.d.ts +25 -0
- package/dist/db/index.d.ts +12 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +84 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +2 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +94 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/router/index.d.ts +33 -0
- package/dist/router/index.js +62 -0
- package/dist/server/api/admin.d.ts +3 -0
- package/dist/server/api/admin.d.ts.map +1 -0
- package/dist/server/api/admin.js +184 -0
- package/dist/server/api/admin.js.map +1 -0
- package/dist/server/api/auth.d.ts +3 -0
- package/dist/server/api/auth.d.ts.map +1 -0
- package/dist/server/api/auth.js +66 -0
- package/dist/server/api/auth.js.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +47 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vlib.css +1 -0
- package/package.json +91 -0
- package/src/components/AdminForm.vue +491 -0
- package/src/components/AdminTable.vue +269 -0
- package/src/components/AppLayout.vue +280 -0
- package/src/components/NavSidebar.vue +176 -0
- package/src/components/TableView.vue +379 -0
- package/src/components/index.ts +13 -0
- package/src/components/types.ts +28 -0
- package/templates/.env.example +4 -0
- package/templates/.prettierignore +3 -0
- package/templates/.prettierrc +6 -0
- package/templates/Dockerfile.ejs +31 -0
- package/templates/docker-compose.prod.yml.ejs +22 -0
- package/templates/docker-compose.yml.ejs +22 -0
- package/templates/eslint.config.mjs +42 -0
- package/templates/index.html.ejs +13 -0
- package/templates/package.json.ejs +44 -0
- package/templates/postcss.config.js.ejs +6 -0
- package/templates/schemas/001-initial.sql +35 -0
- package/templates/scripts/migrate.ts +13 -0
- package/templates/server/index.ts +13 -0
- package/templates/src/App.vue +8 -0
- package/templates/src/main.ts +6 -0
- package/templates/src/router.ts +26 -0
- package/templates/src/routes/_layout.vue +58 -0
- package/templates/src/routes/admin/_layout.vue +8 -0
- package/templates/src/routes/admin/index.vue +88 -0
- package/templates/src/routes/admin/tables/[table]/[id].vue +20 -0
- package/templates/src/routes/admin/tables/[table]/index.vue +10 -0
- package/templates/src/routes/admin/tables/[table]/new.vue +10 -0
- package/templates/src/routes/index.vue +34 -0
- package/templates/src/routes/login.vue +128 -0
- package/templates/src/stores/auth.ts +58 -0
- package/templates/src/styles/main.scss +98 -0
- package/templates/src/styles/variables.scss +7 -0
- package/templates/tailwind.config.js.ejs +27 -0
- package/templates/tsconfig.json.ejs +26 -0
- package/templates/tsconfig.server.json.ejs +17 -0
- package/templates/vite.config.ts.ejs +36 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
+
import type { DB } from '../db/index.js';
|
|
3
|
+
import type { User } from '../types.js';
|
|
4
|
+
declare global {
|
|
5
|
+
namespace Express {
|
|
6
|
+
interface Request {
|
|
7
|
+
user?: User;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
db?: DB;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
declare const SESSION_COOKIE = "vlib-session";
|
|
14
|
+
export declare function createAuthMiddleware(db: DB): RequestHandler;
|
|
15
|
+
export declare function requireAuth(req: Request, res: Response, next: NextFunction): void;
|
|
16
|
+
export declare function noAuth(_req: Request, _res: Response, next: NextFunction): void;
|
|
17
|
+
export { SESSION_COOKIE };
|
|
18
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/auth/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,IAAI,CAAC;YACZ,SAAS,CAAC,EAAE,MAAM,CAAC;YACnB,EAAE,CAAC,EAAE,EAAE,CAAC;SACT;KACF;CACF;AAED,QAAA,MAAM,cAAc,iBAAiB,CAAC;AAEtC,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,EAAE,GAAG,cAAc,CA0C3D;AAED,wBAAgB,WAAW,CACzB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CAMN;AAED,wBAAgB,MAAM,CACpB,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,QAAQ,EACd,IAAI,EAAE,YAAY,GACjB,IAAI,CAEN;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const SESSION_COOKIE = 'vlib-session';
|
|
2
|
+
export function createAuthMiddleware(db) {
|
|
3
|
+
return (req, res, next) => {
|
|
4
|
+
req.db = db;
|
|
5
|
+
const sessionId = req.cookies?.[SESSION_COOKIE];
|
|
6
|
+
if (!sessionId) {
|
|
7
|
+
return next();
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const now = new Date().toISOString();
|
|
11
|
+
const session = db
|
|
12
|
+
.prepare(`SELECT s.id, s.userId, u.id as uid, u.username, u.displayName, u.preferences
|
|
13
|
+
FROM sessions s
|
|
14
|
+
JOIN users u ON s.userId = u.id
|
|
15
|
+
WHERE s.id = ? AND s.expires > ?`)
|
|
16
|
+
.get(sessionId, now);
|
|
17
|
+
if (session) {
|
|
18
|
+
req.sessionId = session.id;
|
|
19
|
+
req.user = {
|
|
20
|
+
id: session.uid,
|
|
21
|
+
username: session.username,
|
|
22
|
+
displayName: session.displayName,
|
|
23
|
+
preferences: JSON.parse(session.preferences || '{}'),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error('Auth middleware error:', err);
|
|
29
|
+
}
|
|
30
|
+
next();
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function requireAuth(req, res, next) {
|
|
34
|
+
if (!req.user) {
|
|
35
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
next();
|
|
39
|
+
}
|
|
40
|
+
export function noAuth(_req, _res, next) {
|
|
41
|
+
next();
|
|
42
|
+
}
|
|
43
|
+
export { SESSION_COOKIE };
|
|
44
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/auth/middleware.ts"],"names":[],"mappings":"AAeA,MAAM,cAAc,GAAG,cAAc,CAAC;AAEtC,MAAM,UAAU,oBAAoB,CAAC,EAAM;IACzC,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC;QACZ,MAAM,SAAS,GAAuB,GAAG,CAAC,OAAO,EAAE,CAAC,cAAc,CAAC,CAAC;QACpE,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACrC,MAAM,OAAO,GAAG,EAAE;iBACf,OAAO,CACN;;;4CAGkC,CACnC;iBACA,GAAG,CAAC,SAAS,EAAE,GAAG,CASR,CAAC;YAEd,IAAI,OAAO,EAAE,CAAC;gBACZ,GAAG,CAAC,SAAS,GAAG,OAAO,CAAC,EAAE,CAAC;gBAC3B,GAAG,CAAC,IAAI,GAAG;oBACT,EAAE,EAAE,OAAO,CAAC,GAAG;oBACf,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,IAAI,IAAI,CAAC;iBACrD,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC;QAC/C,CAAC;QACD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACd,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;QAC3D,OAAO;IACT,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC;AAED,MAAM,UAAU,MAAM,CACpB,IAAa,EACb,IAAc,EACd,IAAkB;IAElB,IAAI,EAAE,CAAC;AACT,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash a password using PBKDF2. Returns a string in the format:
|
|
3
|
+
* pbkdf2:<iterations>:<salt_hex>:<hash_hex>
|
|
4
|
+
*/
|
|
5
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Verify a password against a stored hash string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function verifyPassword(password: string, stored: string): Promise<boolean>;
|
|
10
|
+
//# sourceMappingURL=password.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../src/auth/password.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAW9D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAqBlB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
const ALGORITHM = 'pbkdf2';
|
|
3
|
+
const ITERATIONS = 100000;
|
|
4
|
+
const KEYLEN = 64;
|
|
5
|
+
const DIGEST = 'sha256';
|
|
6
|
+
/**
|
|
7
|
+
* Hash a password using PBKDF2. Returns a string in the format:
|
|
8
|
+
* pbkdf2:<iterations>:<salt_hex>:<hash_hex>
|
|
9
|
+
*/
|
|
10
|
+
export function hashPassword(password) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
13
|
+
crypto.pbkdf2(password, salt, ITERATIONS, KEYLEN, DIGEST, (err, key) => {
|
|
14
|
+
if (err) {
|
|
15
|
+
reject(err);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
resolve(`${ALGORITHM}:${ITERATIONS}:${salt}:${key.toString('hex')}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Verify a password against a stored hash string.
|
|
25
|
+
*/
|
|
26
|
+
export function verifyPassword(password, stored) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const parts = stored.split(':');
|
|
29
|
+
if (parts.length !== 4 || parts[0] !== ALGORITHM) {
|
|
30
|
+
return resolve(false);
|
|
31
|
+
}
|
|
32
|
+
const [, iterStr, salt, expectedHash] = parts;
|
|
33
|
+
const iterations = parseInt(iterStr, 10);
|
|
34
|
+
crypto.pbkdf2(password, salt, iterations, KEYLEN, DIGEST, (err, key) => {
|
|
35
|
+
if (err) {
|
|
36
|
+
reject(err);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
resolve(crypto.timingSafeEqual(Buffer.from(key.toString('hex')), Buffer.from(expectedHash)));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/auth/password.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,MAAM,SAAS,GAAG,QAAQ,CAAC;AAC3B,MAAM,UAAU,GAAG,MAAM,CAAC;AAC1B,MAAM,MAAM,GAAG,EAAE,CAAC;AAClB,MAAM,MAAM,GAAG,QAAQ,CAAC;AAExB;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACpD,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACrE,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,SAAS,IAAI,UAAU,IAAI,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACvE,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAgB,EAChB,MAAc;IAEd,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;YACjD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;QACD,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC;QAC9C,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACrE,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC;iBAAM,CAAC;gBACN,OAAO,CACL,MAAM,CAAC,eAAe,CACpB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAChC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAC1B,CACF,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* vlib scaffold generator CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @vincent99/vlib --name MyApp
|
|
7
|
+
* npx @vincent99/vlib --name MyApp --dir ./my-app
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import ejs from 'ejs';
|
|
13
|
+
import { execSync } from 'child_process';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
// Templates live at <package-root>/templates/
|
|
18
|
+
// When running from dist/, go up one level to the package root
|
|
19
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates');
|
|
20
|
+
const program = new Command();
|
|
21
|
+
program
|
|
22
|
+
.name('vlib')
|
|
23
|
+
.description('Scaffold a new Vue 3 + SQLite webapp using @vincent99/vlib')
|
|
24
|
+
.requiredOption('-n, --name <name>', 'Application name (e.g. "My App")')
|
|
25
|
+
.option('-d, --dir <directory>', 'Target directory (default: ./<kebab-name>)')
|
|
26
|
+
.option('--no-install', 'Skip running yarn install')
|
|
27
|
+
.action(run);
|
|
28
|
+
program.parse();
|
|
29
|
+
async function run(opts) {
|
|
30
|
+
const { name } = opts;
|
|
31
|
+
const kebabName = name
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/\s+/g, '-')
|
|
34
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
35
|
+
const targetDir = opts.dir
|
|
36
|
+
? path.resolve(opts.dir)
|
|
37
|
+
: path.resolve(process.cwd(), kebabName);
|
|
38
|
+
console.log(`\n⚡ Scaffolding "${name}" into ${targetDir}\n`);
|
|
39
|
+
if (fs.existsSync(targetDir)) {
|
|
40
|
+
const files = fs.readdirSync(targetDir);
|
|
41
|
+
if (files.length > 0) {
|
|
42
|
+
console.error(`Error: Target directory "${targetDir}" already exists and is not empty.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Create directory
|
|
47
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
48
|
+
const ctx = { name, kebabName };
|
|
49
|
+
// Walk the templates directory and copy/render each file
|
|
50
|
+
walkAndCopy(TEMPLATES_DIR, targetDir, ctx);
|
|
51
|
+
console.log('\n✓ Files created');
|
|
52
|
+
// Run yarn install
|
|
53
|
+
if (opts.install) {
|
|
54
|
+
console.log('\n📦 Installing dependencies with yarn...\n');
|
|
55
|
+
try {
|
|
56
|
+
execSync('yarn install', { cwd: targetDir, stdio: 'inherit' });
|
|
57
|
+
console.log('\n✓ Dependencies installed');
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
console.warn('\n⚠ yarn install failed — run it manually in the project directory.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Print next steps
|
|
64
|
+
console.log(`
|
|
65
|
+
✅ Done! Your app "${name}" is ready.
|
|
66
|
+
|
|
67
|
+
Next steps:
|
|
68
|
+
cd ${path.relative(process.cwd(), targetDir)}
|
|
69
|
+
cp .env.example .env # configure environment
|
|
70
|
+
yarn migrate # initialize the database
|
|
71
|
+
yarn dev # start dev server
|
|
72
|
+
|
|
73
|
+
# Or with Docker:
|
|
74
|
+
docker compose up # development (with live reloading)
|
|
75
|
+
docker compose -f docker-compose.prod.yml up --build # production
|
|
76
|
+
|
|
77
|
+
Default login: admin / admin
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
function walkAndCopy(srcDir, destDir, ctx) {
|
|
81
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
84
|
+
let destName = entry.name;
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
const destSubDir = path.join(destDir, destName);
|
|
87
|
+
fs.mkdirSync(destSubDir, { recursive: true });
|
|
88
|
+
walkAndCopy(srcPath, destSubDir, ctx);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// EJS templates: render and strip .ejs extension
|
|
92
|
+
const isEjs = entry.name.endsWith('.ejs');
|
|
93
|
+
if (isEjs) {
|
|
94
|
+
destName = entry.name.slice(0, -4);
|
|
95
|
+
}
|
|
96
|
+
const destPath = path.join(destDir, destName);
|
|
97
|
+
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
98
|
+
const rendered = isEjs ? ejs.render(content, ctx) : content;
|
|
99
|
+
fs.writeFileSync(destPath, rendered, 'utf-8');
|
|
100
|
+
console.log(` create ${path.relative(process.cwd(), destPath)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
tableName: string;
|
|
3
|
+
/** Single row ID for edit, undefined for new, or array for multi-edit */
|
|
4
|
+
rowIds?: (string | number)[];
|
|
5
|
+
};
|
|
6
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
tableName: string;
|
|
3
|
+
};
|
|
4
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AppLayoutProps, NavItem } from './types.js';
|
|
2
|
+
export type { AppLayoutProps, NavItem };
|
|
3
|
+
declare function __VLS_template(): {
|
|
4
|
+
attrs: Partial<{}>;
|
|
5
|
+
slots: {
|
|
6
|
+
logo?(_: {}): any;
|
|
7
|
+
sidebar?(_: {}): any;
|
|
8
|
+
default?(_: {}): any;
|
|
9
|
+
};
|
|
10
|
+
refs: {
|
|
11
|
+
userMenuRef: HTMLDivElement;
|
|
12
|
+
};
|
|
13
|
+
rootEl: HTMLDivElement;
|
|
14
|
+
};
|
|
15
|
+
type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
|
|
16
|
+
declare const __VLS_component: import('vue').DefineComponent<AppLayoutProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
17
|
+
logout: () => any;
|
|
18
|
+
}, string, import('vue').PublicProps, Readonly<AppLayoutProps> & Readonly<{
|
|
19
|
+
onLogout?: (() => any) | undefined;
|
|
20
|
+
}>, {
|
|
21
|
+
appName: string;
|
|
22
|
+
user: {
|
|
23
|
+
username: string;
|
|
24
|
+
displayName?: string | null;
|
|
25
|
+
} | null;
|
|
26
|
+
navItems: NavItem[];
|
|
27
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
|
|
28
|
+
userMenuRef: HTMLDivElement;
|
|
29
|
+
}, HTMLDivElement>;
|
|
30
|
+
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
|
|
31
|
+
export default _default;
|
|
32
|
+
type __VLS_WithTemplateSlots<T, S> = T & {
|
|
33
|
+
new (): {
|
|
34
|
+
$slots: S;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NavItem } from './types.js';
|
|
2
|
+
export type { NavItem };
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
items: NavItem[];
|
|
5
|
+
};
|
|
6
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
7
|
+
navigate: () => any;
|
|
8
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
9
|
+
onNavigate?: (() => any) | undefined;
|
|
10
|
+
}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { TableHeader, TableAction } from './types.js';
|
|
2
|
+
export type { TableHeader, TableAction };
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
rows: Record<string, unknown>[];
|
|
5
|
+
headers: TableHeader[];
|
|
6
|
+
idKey?: string;
|
|
7
|
+
total?: number;
|
|
8
|
+
page?: number;
|
|
9
|
+
pageSize?: number;
|
|
10
|
+
actions?: TableAction[];
|
|
11
|
+
};
|
|
12
|
+
declare function __VLS_template(): {
|
|
13
|
+
attrs: Partial<{}>;
|
|
14
|
+
slots: Partial<Record<`cell-${string}`, (_: {
|
|
15
|
+
row: Record<string, unknown>;
|
|
16
|
+
value: unknown;
|
|
17
|
+
}) => any>> & {
|
|
18
|
+
'toolbar-left'?(_: {}): any;
|
|
19
|
+
'toolbar-right'?(_: {}): any;
|
|
20
|
+
'row-actions'?(_: {
|
|
21
|
+
id: string | number;
|
|
22
|
+
row: Record<string, unknown>;
|
|
23
|
+
}): any;
|
|
24
|
+
};
|
|
25
|
+
refs: {};
|
|
26
|
+
rootEl: HTMLDivElement;
|
|
27
|
+
};
|
|
28
|
+
type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
|
|
29
|
+
declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {
|
|
30
|
+
selectedIds: import('vue').Ref<Set<string | number> & Omit<Set<string | number>, keyof Set<any>>, Set<string | number> | (Set<string | number> & Omit<Set<string | number>, keyof Set<any>>)>;
|
|
31
|
+
}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
32
|
+
sort: (key: string, dir: "desc" | "asc") => any;
|
|
33
|
+
"update:page": (page: number) => any;
|
|
34
|
+
action: (key: string, ids: (string | number)[]) => any;
|
|
35
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
36
|
+
onSort?: ((key: string, dir: "desc" | "asc") => any) | undefined;
|
|
37
|
+
"onUpdate:page"?: ((page: number) => any) | undefined;
|
|
38
|
+
onAction?: ((key: string, ids: (string | number)[]) => any) | undefined;
|
|
39
|
+
}>, {
|
|
40
|
+
page: number;
|
|
41
|
+
idKey: string;
|
|
42
|
+
total: number;
|
|
43
|
+
pageSize: number;
|
|
44
|
+
actions: TableAction[];
|
|
45
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
|
|
46
|
+
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
|
|
47
|
+
export default _default;
|
|
48
|
+
type __VLS_WithTemplateSlots<T, S> = T & {
|
|
49
|
+
new (): {
|
|
50
|
+
$slots: S;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as AppLayout } from './AppLayout.vue';
|
|
2
|
+
export { default as NavSidebar } from './NavSidebar.vue';
|
|
3
|
+
export { default as TableView } from './TableView.vue';
|
|
4
|
+
export { default as AdminTable } from './AdminTable.vue';
|
|
5
|
+
export { default as AdminForm } from './AdminForm.vue';
|
|
6
|
+
export type { AppLayoutProps, NavItem, TableHeader, TableAction, } from './types.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface AppLayoutProps {
|
|
2
|
+
appName?: string;
|
|
3
|
+
user?: {
|
|
4
|
+
username: string;
|
|
5
|
+
displayName?: string | null;
|
|
6
|
+
} | null;
|
|
7
|
+
navItems?: NavItem[];
|
|
8
|
+
}
|
|
9
|
+
export interface NavItem {
|
|
10
|
+
label: string;
|
|
11
|
+
to?: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
children?: NavItem[];
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface TableHeader {
|
|
17
|
+
key: string;
|
|
18
|
+
label: string;
|
|
19
|
+
sortable?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface TableAction {
|
|
22
|
+
key: string;
|
|
23
|
+
label: string;
|
|
24
|
+
variant?: 'default' | 'danger' | 'primary';
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { TableInfo } from '../types.js';
|
|
3
|
+
export type DB = Database.Database;
|
|
4
|
+
export declare function getDb(): DB;
|
|
5
|
+
export declare function initDb(dbPath: string): DB;
|
|
6
|
+
export declare function getSchemaVersion(db: DB): number;
|
|
7
|
+
export declare function setSchemaVersion(db: DB, version: number): void;
|
|
8
|
+
export declare function getLatestSchemaNumber(schemasDir: string): number;
|
|
9
|
+
export declare function assertSchemaUpToDate(db: DB, schemasDir: string): void;
|
|
10
|
+
export declare function listTables(db: DB): string[];
|
|
11
|
+
export declare function getTableInfo(db: DB, tableName: string): TableInfo;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAGtC,OAAO,KAAK,EAAE,SAAS,EAA2B,MAAM,aAAa,CAAC;AAEtE,MAAM,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC;AAInC,wBAAgB,KAAK,IAAI,EAAE,CAK1B;AAED,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,EAAE,CASzC;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,GAAG,MAAM,CAY/C;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAO9D;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAShE;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CASrE;AAID,wBAAgB,UAAU,CAAC,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,CAO3C;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,CASjE"}
|
package/dist/db/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
let _db = null;
|
|
5
|
+
export function getDb() {
|
|
6
|
+
if (!_db) {
|
|
7
|
+
throw new Error('Database not initialized. Call initDb() first.');
|
|
8
|
+
}
|
|
9
|
+
return _db;
|
|
10
|
+
}
|
|
11
|
+
export function initDb(dbPath) {
|
|
12
|
+
const dir = path.dirname(dbPath);
|
|
13
|
+
if (!fs.existsSync(dir)) {
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
_db = new Database(dbPath);
|
|
17
|
+
_db.pragma('journal_mode = WAL');
|
|
18
|
+
_db.pragma('foreign_keys = ON');
|
|
19
|
+
return _db;
|
|
20
|
+
}
|
|
21
|
+
export function getSchemaVersion(db) {
|
|
22
|
+
try {
|
|
23
|
+
const row = db
|
|
24
|
+
.prepare(`SELECT value FROM settings WHERE key = 'schema_version'`)
|
|
25
|
+
.get();
|
|
26
|
+
if (!row) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
return parseInt(JSON.parse(row.value), 10) || 0;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function setSchemaVersion(db, version) {
|
|
36
|
+
db.prepare(`
|
|
37
|
+
INSERT INTO settings (key, value) VALUES ('schema_version', ?)
|
|
38
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
39
|
+
`).run(JSON.stringify(version));
|
|
40
|
+
}
|
|
41
|
+
export function getLatestSchemaNumber(schemasDir) {
|
|
42
|
+
if (!fs.existsSync(schemasDir)) {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
const files = fs.readdirSync(schemasDir);
|
|
46
|
+
const nums = files
|
|
47
|
+
.filter((f) => /^\d{3}-.*\.sql$/.test(f))
|
|
48
|
+
.map((f) => parseInt(f.slice(0, 3), 10));
|
|
49
|
+
return nums.length > 0 ? Math.max(...nums) : 0;
|
|
50
|
+
}
|
|
51
|
+
export function assertSchemaUpToDate(db, schemasDir) {
|
|
52
|
+
const latest = getLatestSchemaNumber(schemasDir);
|
|
53
|
+
const current = getSchemaVersion(db);
|
|
54
|
+
if (current !== latest) {
|
|
55
|
+
throw new Error(`Database schema mismatch: DB is at version ${current}, but latest schema file is ${latest}. ` +
|
|
56
|
+
`Run \`yarn migrate\` to update.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ---------- table introspection ----------
|
|
60
|
+
export function listTables(db) {
|
|
61
|
+
const rows = db
|
|
62
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`)
|
|
63
|
+
.all();
|
|
64
|
+
return rows.map((r) => r.name);
|
|
65
|
+
}
|
|
66
|
+
export function getTableInfo(db, tableName) {
|
|
67
|
+
const columns = db
|
|
68
|
+
.prepare(`PRAGMA table_info("${tableName}")`)
|
|
69
|
+
.all();
|
|
70
|
+
const foreignKeys = db
|
|
71
|
+
.prepare(`PRAGMA foreign_key_list("${tableName}")`)
|
|
72
|
+
.all();
|
|
73
|
+
const isJoinTable = detectJoinTable(columns, foreignKeys);
|
|
74
|
+
return { name: tableName, columns, foreignKeys, isJoinTable };
|
|
75
|
+
}
|
|
76
|
+
function detectJoinTable(columns, foreignKeys) {
|
|
77
|
+
if (columns.length > 4) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const fkFromCols = new Set(foreignKeys.map((fk) => fk.from));
|
|
81
|
+
const nonFkCols = columns.filter((c) => c.pk === 0 && !fkFromCols.has(c.name));
|
|
82
|
+
return nonFkCols.length === 0 && foreignKeys.length >= 2;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":"AA+DA,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CA6Df"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { initDb, getSchemaVersion, setSchemaVersion, getLatestSchemaNumber, } from './index.js';
|
|
5
|
+
function getSchemaFiles(schemasDir) {
|
|
6
|
+
if (!fs.existsSync(schemasDir)) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
return fs
|
|
10
|
+
.readdirSync(schemasDir)
|
|
11
|
+
.filter((f) => /^\d{3}-.*\.sql$/.test(f))
|
|
12
|
+
.map((f) => ({ num: parseInt(f.slice(0, 3), 10), file: f }))
|
|
13
|
+
.sort((a, b) => a.num - b.num);
|
|
14
|
+
}
|
|
15
|
+
function backupDb(dbPath, currentVersion) {
|
|
16
|
+
const backupsDir = path.resolve(path.dirname(dbPath), '..', 'backups');
|
|
17
|
+
if (!fs.existsSync(backupsDir)) {
|
|
18
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
21
|
+
const backupName = `backup-v${currentVersion}-${timestamp}.db`;
|
|
22
|
+
const backupPath = path.join(backupsDir, backupName);
|
|
23
|
+
fs.copyFileSync(dbPath, backupPath);
|
|
24
|
+
console.log(` Backed up DB to: ${backupPath}`);
|
|
25
|
+
}
|
|
26
|
+
function hashPasswordSync(password) {
|
|
27
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
28
|
+
const hash = crypto
|
|
29
|
+
.pbkdf2Sync(password, salt, 100000, 64, 'sha256')
|
|
30
|
+
.toString('hex');
|
|
31
|
+
return `pbkdf2:100000:${salt}:${hash}`;
|
|
32
|
+
}
|
|
33
|
+
function createInitialAdminUser(db) {
|
|
34
|
+
const existing = db
|
|
35
|
+
.prepare(`SELECT id FROM users WHERE username = 'admin'`)
|
|
36
|
+
.get();
|
|
37
|
+
if (existing) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const hash = hashPasswordSync('admin');
|
|
41
|
+
db.prepare(`
|
|
42
|
+
INSERT INTO users (username, password, displayName, preferences)
|
|
43
|
+
VALUES ('admin', ?, 'Administrator', '{}')
|
|
44
|
+
`).run(hash);
|
|
45
|
+
console.log(' Created initial admin user (username: admin, password: admin)');
|
|
46
|
+
}
|
|
47
|
+
export async function runMigrations(dbPath, schemasDir) {
|
|
48
|
+
console.log(`\nMigration starting...`);
|
|
49
|
+
console.log(` DB: ${dbPath}`);
|
|
50
|
+
console.log(` Schemas: ${schemasDir}`);
|
|
51
|
+
const db = initDb(dbPath);
|
|
52
|
+
const schemaFiles = getSchemaFiles(schemasDir);
|
|
53
|
+
if (schemaFiles.length === 0) {
|
|
54
|
+
console.log(' No schema files found.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const latest = getLatestSchemaNumber(schemasDir);
|
|
58
|
+
let currentVersion = getSchemaVersion(db);
|
|
59
|
+
console.log(` Current schema version: ${currentVersion}`);
|
|
60
|
+
console.log(` Latest schema version: ${latest}`);
|
|
61
|
+
if (currentVersion === latest) {
|
|
62
|
+
console.log(' Database is already up to date.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Backup before migrating (only if DB file exists with data)
|
|
66
|
+
if (fs.existsSync(dbPath) &&
|
|
67
|
+
fs.statSync(dbPath).size > 0 &&
|
|
68
|
+
currentVersion > 0) {
|
|
69
|
+
backupDb(dbPath, currentVersion);
|
|
70
|
+
}
|
|
71
|
+
const pending = schemaFiles.filter((s) => s.num > currentVersion);
|
|
72
|
+
for (const { num, file } of pending) {
|
|
73
|
+
const filePath = path.join(schemasDir, file);
|
|
74
|
+
const sql = fs.readFileSync(filePath, 'utf-8');
|
|
75
|
+
console.log(`\n Applying schema ${num}: ${file}`);
|
|
76
|
+
try {
|
|
77
|
+
db.exec(sql);
|
|
78
|
+
// Special post-processing for schema 1: seed admin user
|
|
79
|
+
if (num === 1) {
|
|
80
|
+
createInitialAdminUser(db);
|
|
81
|
+
}
|
|
82
|
+
setSchemaVersion(db, num);
|
|
83
|
+
currentVersion = num;
|
|
84
|
+
console.log(` ✓ Applied schema ${num}`);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error(` ✗ Error applying schema ${num}:`, err);
|
|
88
|
+
console.error(` Migration stopped at version ${currentVersion}.`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log(`\nMigration complete. Database is now at version ${currentVersion}.`);
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED