@aruvili/api 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/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +34 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +3 -0
- package/dist/context.js.map +1 -0
- package/dist/controllers/index.d.ts +39 -0
- package/dist/controllers/index.d.ts.map +1 -0
- package/dist/controllers/index.js +39 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/db.d.ts +6 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +74 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +15 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +93 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/body-limit.d.ts +9 -0
- package/dist/middleware/body-limit.d.ts.map +1 -0
- package/dist/middleware/body-limit.js +15 -0
- package/dist/middleware/body-limit.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +6 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +40 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/rbac.d.ts +10 -0
- package/dist/middleware/rbac.d.ts.map +1 -0
- package/dist/middleware/rbac.js +61 -0
- package/dist/middleware/rbac.js.map +1 -0
- package/dist/middleware/tenant.d.ts +3 -0
- package/dist/middleware/tenant.d.ts.map +1 -0
- package/dist/middleware/tenant.js +19 -0
- package/dist/middleware/tenant.js.map +1 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +112 -0
- package/dist/registry.js.map +1 -0
- package/dist/routes/auth.d.ts +3 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +141 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/crud.d.ts +7 -0
- package/dist/routes/crud.d.ts.map +1 -0
- package/dist/routes/crud.js +845 -0
- package/dist/routes/crud.js.map +1 -0
- package/dist/routes/files.d.ts +7 -0
- package/dist/routes/files.d.ts.map +1 -0
- package/dist/routes/files.js +123 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/meta.d.ts +3 -0
- package/dist/routes/meta.d.ts.map +1 -0
- package/dist/routes/meta.js +352 -0
- package/dist/routes/meta.js.map +1 -0
- package/dist/scheduler.d.ts +33 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +97 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/utils/link-validator.d.ts +7 -0
- package/dist/utils/link-validator.d.ts.map +1 -0
- package/dist/utils/link-validator.js +33 -0
- package/dist/utils/link-validator.js.map +1 -0
- package/dist/utils/resolver.d.ts +5 -0
- package/dist/utils/resolver.d.ts.map +1 -0
- package/dist/utils/resolver.js +58 -0
- package/dist/utils/resolver.js.map +1 -0
- package/package.json +24 -0
- package/src/api.test.ts +362 -0
- package/src/config.d.ts +22 -0
- package/src/config.d.ts.map +1 -0
- package/src/config.js +34 -0
- package/src/config.js.map +1 -0
- package/src/config.ts +38 -0
- package/src/context.d.ts +7 -0
- package/src/context.d.ts.map +1 -0
- package/src/context.js +3 -0
- package/src/context.js.map +1 -0
- package/src/context.ts +8 -0
- package/src/controllers/index.d.ts +39 -0
- package/src/controllers/index.d.ts.map +1 -0
- package/src/controllers/index.js +39 -0
- package/src/controllers/index.js.map +1 -0
- package/src/controllers/index.ts +51 -0
- package/src/db.d.ts +6 -0
- package/src/db.d.ts.map +1 -0
- package/src/db.js +74 -0
- package/src/db.js.map +1 -0
- package/src/db.ts +73 -0
- package/src/index.ts +178 -0
- package/src/integration.test.ts +453 -0
- package/src/middleware/auth.d.ts +15 -0
- package/src/middleware/auth.d.ts.map +1 -0
- package/src/middleware/auth.js +93 -0
- package/src/middleware/auth.js.map +1 -0
- package/src/middleware/auth.ts +109 -0
- package/src/middleware/body-limit.d.ts +9 -0
- package/src/middleware/body-limit.d.ts.map +1 -0
- package/src/middleware/body-limit.js +15 -0
- package/src/middleware/body-limit.js.map +1 -0
- package/src/middleware/body-limit.ts +16 -0
- package/src/middleware/rate-limit.d.ts +6 -0
- package/src/middleware/rate-limit.d.ts.map +1 -0
- package/src/middleware/rate-limit.js +40 -0
- package/src/middleware/rate-limit.js.map +1 -0
- package/src/middleware/rate-limit.ts +47 -0
- package/src/middleware/rbac.d.ts +10 -0
- package/src/middleware/rbac.d.ts.map +1 -0
- package/src/middleware/rbac.js +61 -0
- package/src/middleware/rbac.js.map +1 -0
- package/src/middleware/rbac.ts +71 -0
- package/src/middleware/tenant.d.ts +3 -0
- package/src/middleware/tenant.d.ts.map +1 -0
- package/src/middleware/tenant.js +19 -0
- package/src/middleware/tenant.js.map +1 -0
- package/src/middleware/tenant.ts +24 -0
- package/src/registry.d.ts +26 -0
- package/src/registry.d.ts.map +1 -0
- package/src/registry.js +112 -0
- package/src/registry.js.map +1 -0
- package/src/registry.ts +123 -0
- package/src/routes/auth.d.ts +3 -0
- package/src/routes/auth.d.ts.map +1 -0
- package/src/routes/auth.js +141 -0
- package/src/routes/auth.js.map +1 -0
- package/src/routes/auth.ts +164 -0
- package/src/routes/crud.d.ts +7 -0
- package/src/routes/crud.d.ts.map +1 -0
- package/src/routes/crud.js +845 -0
- package/src/routes/crud.js.map +1 -0
- package/src/routes/crud.ts +1029 -0
- package/src/routes/files.d.ts +7 -0
- package/src/routes/files.d.ts.map +1 -0
- package/src/routes/files.js +123 -0
- package/src/routes/files.js.map +1 -0
- package/src/routes/files.ts +143 -0
- package/src/routes/meta.d.ts +3 -0
- package/src/routes/meta.d.ts.map +1 -0
- package/src/routes/meta.js +352 -0
- package/src/routes/meta.js.map +1 -0
- package/src/routes/meta.ts +448 -0
- package/src/scheduler.ts +118 -0
- package/src/utils/link-validator.d.ts +7 -0
- package/src/utils/link-validator.d.ts.map +1 -0
- package/src/utils/link-validator.js +33 -0
- package/src/utils/link-validator.js.map +1 -0
- package/src/utils/link-validator.ts +45 -0
- package/src/utils/resolver.d.ts +5 -0
- package/src/utils/resolver.d.ts.map +1 -0
- package/src/utils/resolver.js +58 -0
- package/src/utils/resolver.js.map +1 -0
- package/src/utils/resolver.ts +65 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAM5B,eAAO,MAAM,WAAW;eAAyB;QAAE,IAAI,EAAE,GAAG,CAAA;KAAE;yCAAK,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { query } from '../db.js';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
export const filesRouter = new Hono();
|
|
7
|
+
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
|
|
8
|
+
// Ensure upload directory exists
|
|
9
|
+
async function ensureUploadsDir() {
|
|
10
|
+
try {
|
|
11
|
+
await fs.mkdir(UPLOADS_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
console.error('Failed to create uploads directory:', err);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
ensureUploadsDir();
|
|
18
|
+
/**
|
|
19
|
+
* POST /api/files/upload
|
|
20
|
+
* Upload a file and attach it optionally to a document field.
|
|
21
|
+
*/
|
|
22
|
+
filesRouter.post('/upload', async (c) => {
|
|
23
|
+
const user = c.get('user');
|
|
24
|
+
if (!user || user.roles.includes('Guest')) {
|
|
25
|
+
return c.json({ error: 'Unauthorized: Guest users cannot upload files.' }, 401);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const formData = await c.req.parseBody();
|
|
29
|
+
const file = formData['file'];
|
|
30
|
+
if (!file || !(file instanceof File)) {
|
|
31
|
+
return c.json({ error: 'No file uploaded under form key "file".' }, 400);
|
|
32
|
+
}
|
|
33
|
+
const attachedToDocType = formData['attached_to_doctype'];
|
|
34
|
+
const attachedToName = formData['attached_to_name'];
|
|
35
|
+
const attachedToField = formData['attached_to_field'];
|
|
36
|
+
// Convert file to buffer
|
|
37
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
38
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
39
|
+
// Generate unique name
|
|
40
|
+
const fileUuid = crypto.randomUUID();
|
|
41
|
+
const ext = path.extname(file.name);
|
|
42
|
+
const safeName = `${fileUuid}${ext}`;
|
|
43
|
+
const destinationPath = path.join(UPLOADS_DIR, safeName);
|
|
44
|
+
// Save to disk
|
|
45
|
+
await fs.writeFile(destinationPath, buffer);
|
|
46
|
+
const fileUrl = `/api/files/download/${safeName}`;
|
|
47
|
+
const fileSize = file.size;
|
|
48
|
+
const mimeType = file.type || 'application/octet-stream';
|
|
49
|
+
// Save record to DB
|
|
50
|
+
const res = await query(`INSERT INTO _files (
|
|
51
|
+
name, file_name, file_url, file_size, mime_type,
|
|
52
|
+
attached_to_doctype, attached_to_name, attached_to_field, created_by
|
|
53
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`, [
|
|
54
|
+
fileUuid, file.name, fileUrl, fileSize, mimeType,
|
|
55
|
+
attachedToDocType || null, attachedToName || null, attachedToField || null, user.email
|
|
56
|
+
]);
|
|
57
|
+
return c.json({ success: true, file: res.rows[0] }, 201);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return c.json({ error: 'File upload failed', details: err.message }, 500);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* GET /api/files/download/:name
|
|
65
|
+
* Download/stream a file.
|
|
66
|
+
*/
|
|
67
|
+
filesRouter.get('/download/:name', async (c) => {
|
|
68
|
+
const name = c.req.param('name');
|
|
69
|
+
// Prevent directory traversal attacks
|
|
70
|
+
const safeName = path.basename(name);
|
|
71
|
+
const filePath = path.join(UPLOADS_DIR, safeName);
|
|
72
|
+
try {
|
|
73
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
74
|
+
// Resolve MIME Type from DB
|
|
75
|
+
const res = await query('SELECT mime_type, file_name FROM _files WHERE name = $1 OR file_url LIKE $2', [
|
|
76
|
+
safeName.split('.')[0],
|
|
77
|
+
`%/download/${safeName}`
|
|
78
|
+
]);
|
|
79
|
+
const mimeType = res.rows[0]?.mime_type || 'application/octet-stream';
|
|
80
|
+
const originalName = res.rows[0]?.file_name || safeName;
|
|
81
|
+
c.header('Content-Type', mimeType);
|
|
82
|
+
c.header('Content-Disposition', `inline; filename="${originalName}"`);
|
|
83
|
+
return c.body(fileBuffer);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
return c.json({ error: 'File not found' }, 404);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* DELETE /api/files/:name
|
|
91
|
+
* Delete an attachment.
|
|
92
|
+
*/
|
|
93
|
+
filesRouter.delete('/:name', async (c) => {
|
|
94
|
+
const user = c.get('user');
|
|
95
|
+
if (!user || user.roles.includes('Guest')) {
|
|
96
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
97
|
+
}
|
|
98
|
+
const name = c.req.param('name');
|
|
99
|
+
try {
|
|
100
|
+
const res = await query('SELECT * FROM _files WHERE name = $1', [name]);
|
|
101
|
+
if (res.rows.length === 0) {
|
|
102
|
+
return c.json({ error: 'File attachment record not found.' }, 404);
|
|
103
|
+
}
|
|
104
|
+
const record = res.rows[0];
|
|
105
|
+
// Delete from disk
|
|
106
|
+
const safeName = path.basename(record.file_url);
|
|
107
|
+
const filePath = path.join(UPLOADS_DIR, safeName);
|
|
108
|
+
try {
|
|
109
|
+
await fs.unlink(filePath);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
// Log error but proceed if file was already removed from disk
|
|
113
|
+
console.warn(`File file not found on disk: ${filePath}`);
|
|
114
|
+
}
|
|
115
|
+
// Delete record from DB
|
|
116
|
+
await query('DELETE FROM _files WHERE name = $1', [name]);
|
|
117
|
+
return c.json({ success: true, message: 'Attachment deleted successfully.' });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
return c.json({ error: 'Failed to delete attachment', details: err.message }, 500);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { query } from '../db.js';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
export const filesRouter = new Hono<{ Variables: { user: any } }>();
|
|
8
|
+
|
|
9
|
+
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
|
|
10
|
+
|
|
11
|
+
// Ensure upload directory exists
|
|
12
|
+
async function ensureUploadsDir() {
|
|
13
|
+
try {
|
|
14
|
+
await fs.mkdir(UPLOADS_DIR, { recursive: true });
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error('Failed to create uploads directory:', err);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
ensureUploadsDir();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* POST /api/files/upload
|
|
23
|
+
* Upload a file and attach it optionally to a document field.
|
|
24
|
+
*/
|
|
25
|
+
filesRouter.post('/upload', async (c) => {
|
|
26
|
+
const user = c.get('user');
|
|
27
|
+
if (!user || user.roles.includes('Guest')) {
|
|
28
|
+
return c.json({ error: 'Unauthorized: Guest users cannot upload files.' }, 401);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const formData = await c.req.parseBody();
|
|
33
|
+
const file = formData['file'];
|
|
34
|
+
|
|
35
|
+
if (!file || !(file instanceof File)) {
|
|
36
|
+
return c.json({ error: 'No file uploaded under form key "file".' }, 400);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const attachedToDocType = formData['attached_to_doctype'] as string | undefined;
|
|
40
|
+
const attachedToName = formData['attached_to_name'] as string | undefined;
|
|
41
|
+
const attachedToField = formData['attached_to_field'] as string | undefined;
|
|
42
|
+
|
|
43
|
+
// Convert file to buffer
|
|
44
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
45
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
46
|
+
|
|
47
|
+
// Generate unique name
|
|
48
|
+
const fileUuid = crypto.randomUUID();
|
|
49
|
+
const ext = path.extname(file.name);
|
|
50
|
+
const safeName = `${fileUuid}${ext}`;
|
|
51
|
+
const destinationPath = path.join(UPLOADS_DIR, safeName);
|
|
52
|
+
|
|
53
|
+
// Save to disk
|
|
54
|
+
await fs.writeFile(destinationPath, buffer);
|
|
55
|
+
|
|
56
|
+
const fileUrl = `/api/files/download/${safeName}`;
|
|
57
|
+
const fileSize = file.size;
|
|
58
|
+
const mimeType = file.type || 'application/octet-stream';
|
|
59
|
+
|
|
60
|
+
// Save record to DB
|
|
61
|
+
const res = await query(
|
|
62
|
+
`INSERT INTO _files (
|
|
63
|
+
name, file_name, file_url, file_size, mime_type,
|
|
64
|
+
attached_to_doctype, attached_to_name, attached_to_field, created_by
|
|
65
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`,
|
|
66
|
+
[
|
|
67
|
+
fileUuid, file.name, fileUrl, fileSize, mimeType,
|
|
68
|
+
attachedToDocType || null, attachedToName || null, attachedToField || null, user.email
|
|
69
|
+
]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return c.json({ success: true, file: res.rows[0] }, 201);
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
return c.json({ error: 'File upload failed', details: err.message }, 500);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* GET /api/files/download/:name
|
|
80
|
+
* Download/stream a file.
|
|
81
|
+
*/
|
|
82
|
+
filesRouter.get('/download/:name', async (c) => {
|
|
83
|
+
const name = c.req.param('name');
|
|
84
|
+
// Prevent directory traversal attacks
|
|
85
|
+
const safeName = path.basename(name);
|
|
86
|
+
const filePath = path.join(UPLOADS_DIR, safeName);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
90
|
+
|
|
91
|
+
// Resolve MIME Type from DB
|
|
92
|
+
const res = await query('SELECT mime_type, file_name FROM _files WHERE name = $1 OR file_url LIKE $2', [
|
|
93
|
+
safeName.split('.')[0],
|
|
94
|
+
`%/download/${safeName}`
|
|
95
|
+
]);
|
|
96
|
+
const mimeType = res.rows[0]?.mime_type || 'application/octet-stream';
|
|
97
|
+
const originalName = res.rows[0]?.file_name || safeName;
|
|
98
|
+
|
|
99
|
+
c.header('Content-Type', mimeType);
|
|
100
|
+
c.header('Content-Disposition', `inline; filename="${originalName}"`);
|
|
101
|
+
return c.body(fileBuffer);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return c.json({ error: 'File not found' }, 404);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* DELETE /api/files/:name
|
|
109
|
+
* Delete an attachment.
|
|
110
|
+
*/
|
|
111
|
+
filesRouter.delete('/:name', async (c) => {
|
|
112
|
+
const user = c.get('user');
|
|
113
|
+
if (!user || user.roles.includes('Guest')) {
|
|
114
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const name = c.req.param('name');
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const res = await query('SELECT * FROM _files WHERE name = $1', [name]);
|
|
121
|
+
if (res.rows.length === 0) {
|
|
122
|
+
return c.json({ error: 'File attachment record not found.' }, 404);
|
|
123
|
+
}
|
|
124
|
+
const record = res.rows[0];
|
|
125
|
+
|
|
126
|
+
// Delete from disk
|
|
127
|
+
const safeName = path.basename(record.file_url);
|
|
128
|
+
const filePath = path.join(UPLOADS_DIR, safeName);
|
|
129
|
+
try {
|
|
130
|
+
await fs.unlink(filePath);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
// Log error but proceed if file was already removed from disk
|
|
133
|
+
console.warn(`File file not found on disk: ${filePath}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Delete record from DB
|
|
137
|
+
await query('DELETE FROM _files WHERE name = $1', [name]);
|
|
138
|
+
|
|
139
|
+
return c.json({ success: true, message: 'Attachment deleted successfully.' });
|
|
140
|
+
} catch (err: any) {
|
|
141
|
+
return c.json({ error: 'Failed to delete attachment', details: err.message }, 500);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["meta.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,eAAO,MAAM,UAAU,4EAAa,CAAC"}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { validateDocType, generateCreateTableDDL, generateAlterTableDDL, getTableName } from '@meta/core';
|
|
3
|
+
import { query, withTransaction } from '../db.js';
|
|
4
|
+
import { registry } from '../registry.js';
|
|
5
|
+
export const metaRouter = new Hono();
|
|
6
|
+
/**
|
|
7
|
+
* Helper to fetch existing columns in target table from postgres database catalog.
|
|
8
|
+
*/
|
|
9
|
+
async function getExistingColumns(client, tableName, schema) {
|
|
10
|
+
const sql = `
|
|
11
|
+
SELECT
|
|
12
|
+
column_name AS "columnName",
|
|
13
|
+
data_type AS "dataType",
|
|
14
|
+
is_nullable = 'YES' AS "isNullable",
|
|
15
|
+
character_maximum_length AS "characterMaximumLength"
|
|
16
|
+
FROM information_schema.columns
|
|
17
|
+
WHERE table_name = $1 AND table_schema = $2
|
|
18
|
+
`;
|
|
19
|
+
const res = await client.query(sql, [tableName, schema]);
|
|
20
|
+
return res.rows;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Helper to execute schema synchronization across all registered tenant schemas.
|
|
24
|
+
*/
|
|
25
|
+
async function syncDatabaseSchema(client, definition) {
|
|
26
|
+
const tableName = getTableName(definition.name);
|
|
27
|
+
// Fetch all active tenants
|
|
28
|
+
const tenantsRes = await client.query('SELECT id FROM _tenants');
|
|
29
|
+
const tenantIds = tenantsRes.rows.map((r) => r.id);
|
|
30
|
+
// Sync both public schema and all tenant schemas
|
|
31
|
+
const schemas = Array.from(new Set(['public', ...tenantIds]));
|
|
32
|
+
let totalDdlExecuted = [];
|
|
33
|
+
for (const schema of schemas) {
|
|
34
|
+
// 1. Temporarily swap search path to target schema
|
|
35
|
+
await client.query(`SET search_path TO ${schema}, public`);
|
|
36
|
+
// 2. Check if table exists in the target schema
|
|
37
|
+
const checkTableRes = await client.query(`SELECT EXISTS (
|
|
38
|
+
SELECT FROM information_schema.tables
|
|
39
|
+
WHERE table_name = $1 AND table_schema = $2
|
|
40
|
+
)`, [tableName, schema]);
|
|
41
|
+
const tableExists = checkTableRes.rows[0].exists;
|
|
42
|
+
let ddl = [];
|
|
43
|
+
if (!tableExists) {
|
|
44
|
+
ddl = generateCreateTableDDL(definition);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const cols = await getExistingColumns(client, tableName, schema);
|
|
48
|
+
ddl = generateAlterTableDDL(definition, cols);
|
|
49
|
+
}
|
|
50
|
+
// 3. Run migration DDL
|
|
51
|
+
for (const sql of ddl) {
|
|
52
|
+
await client.query(sql);
|
|
53
|
+
totalDdlExecuted.push(`[${schema}] ${sql}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return totalDdlExecuted;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* POST /api/meta/doctypes
|
|
60
|
+
* Create a new DocType. Fails with 409 Conflict if duplicate.
|
|
61
|
+
*/
|
|
62
|
+
metaRouter.post('/doctypes', async (c) => {
|
|
63
|
+
const definition = await c.req.json();
|
|
64
|
+
// 1. Perform structural integrity checks on the submitted definition
|
|
65
|
+
const valResult = validateDocType(definition);
|
|
66
|
+
if (!valResult.valid) {
|
|
67
|
+
return c.json({ error: 'Schema validation failed', details: valResult.errors }, 400);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const output = await withTransaction(async (client) => {
|
|
71
|
+
// 2. Check if DocType already exists to satisfy Task 2.1
|
|
72
|
+
const checkExists = await client.query('SELECT 1 FROM _doctype_meta WHERE name = $1', [definition.name]);
|
|
73
|
+
if (checkExists.rows.length > 0) {
|
|
74
|
+
throw new Error(`Conflict: DocType '${definition.name}' already exists.`);
|
|
75
|
+
}
|
|
76
|
+
// 3. Insert active metadata registry record
|
|
77
|
+
await client.query(`INSERT INTO _doctype_meta (name, definition, updated_at)
|
|
78
|
+
VALUES ($1, $2, NOW())`, [definition.name, JSON.stringify(definition)]);
|
|
79
|
+
// 4. Perform dynamic schema synchronization across all tenant databases
|
|
80
|
+
const ddlExecuted = await syncDatabaseSchema(client, definition);
|
|
81
|
+
// 5. Log migration history
|
|
82
|
+
if (ddlExecuted.length > 0) {
|
|
83
|
+
await client.query(`INSERT INTO _doctype_migration_history (doctype_name, version, ddl_executed, executed_by)
|
|
84
|
+
VALUES ($1, 1, $2, $3)`, [definition.name, ddlExecuted.join('\n'), 'System Admin']);
|
|
85
|
+
}
|
|
86
|
+
// 6. Log version history tracking (Task 2.3)
|
|
87
|
+
await client.query(`INSERT INTO _doctype_definition_history (doctype_name, version, definition, changed_by, ddl_applied, notes)
|
|
88
|
+
VALUES ($1, 1, $2, $3, $4, $5)`, [
|
|
89
|
+
definition.name,
|
|
90
|
+
JSON.stringify(definition),
|
|
91
|
+
'System Admin',
|
|
92
|
+
ddlExecuted.join('\n'),
|
|
93
|
+
'Initial definition creation'
|
|
94
|
+
]);
|
|
95
|
+
// Invalidate cache immediately
|
|
96
|
+
registry.invalidate(definition.name);
|
|
97
|
+
return {
|
|
98
|
+
doctype: definition.name,
|
|
99
|
+
table: getTableName(definition.name),
|
|
100
|
+
action: 'CREATED',
|
|
101
|
+
version: 1,
|
|
102
|
+
changes_applied: ddlExecuted
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
return c.json(output);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err.message.includes('Conflict:')) {
|
|
109
|
+
return c.json({ error: err.message }, 409);
|
|
110
|
+
}
|
|
111
|
+
console.error('Doctype creation failed:', err);
|
|
112
|
+
return c.json({ error: 'Failed to create database metadata', details: err.message }, 500);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
/**
|
|
116
|
+
* PUT /api/meta/doctypes/:name
|
|
117
|
+
* Update an existing DocType definition.
|
|
118
|
+
*/
|
|
119
|
+
metaRouter.put('/doctypes/:name', async (c) => {
|
|
120
|
+
const name = c.req.param('name');
|
|
121
|
+
const definition = await c.req.json();
|
|
122
|
+
if (definition.name !== name) {
|
|
123
|
+
return c.json({ error: 'Doctype name mismatch between URL and payload' }, 400);
|
|
124
|
+
}
|
|
125
|
+
// 1. Perform structural integrity checks on the submitted definition
|
|
126
|
+
const valResult = validateDocType(definition);
|
|
127
|
+
if (!valResult.valid) {
|
|
128
|
+
return c.json({ error: 'Schema validation failed', details: valResult.errors }, 400);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const output = await withTransaction(async (client) => {
|
|
132
|
+
// 2. Verify existence
|
|
133
|
+
const checkExists = await client.query('SELECT 1 FROM _doctype_meta WHERE name = $1', [name]);
|
|
134
|
+
if (checkExists.rows.length === 0) {
|
|
135
|
+
throw new Error(`NotFound: DocType '${name}' does not exist.`);
|
|
136
|
+
}
|
|
137
|
+
// 3. Compute next version number
|
|
138
|
+
const versionRes = await client.query('SELECT COALESCE(MAX(version), 0) + 1 AS next FROM _doctype_definition_history WHERE doctype_name = $1', [name]);
|
|
139
|
+
const nextVersion = versionRes.rows[0].next;
|
|
140
|
+
// 4. Perform dynamic schema synchronization across all tenant databases
|
|
141
|
+
const ddlExecuted = await syncDatabaseSchema(client, definition);
|
|
142
|
+
// 5. Update active registry
|
|
143
|
+
await client.query(`UPDATE _doctype_meta SET definition = $2, updated_at = NOW() WHERE name = $1`, [name, JSON.stringify(definition)]);
|
|
144
|
+
// 6. Log migration history
|
|
145
|
+
if (ddlExecuted.length > 0) {
|
|
146
|
+
await client.query(`INSERT INTO _doctype_migration_history (doctype_name, version, ddl_executed, executed_by)
|
|
147
|
+
VALUES ($1, $2, $3, $4)`, [name, nextVersion, ddlExecuted.join('\n'), 'System Admin']);
|
|
148
|
+
}
|
|
149
|
+
// 7. Log version history tracking (Task 2.3)
|
|
150
|
+
await client.query(`INSERT INTO _doctype_definition_history (doctype_name, version, definition, changed_by, ddl_applied, notes)
|
|
151
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [
|
|
152
|
+
name,
|
|
153
|
+
nextVersion,
|
|
154
|
+
JSON.stringify(definition),
|
|
155
|
+
'System Admin',
|
|
156
|
+
ddlExecuted.join('\n'),
|
|
157
|
+
'Updated definition schema'
|
|
158
|
+
]);
|
|
159
|
+
// Invalidate cache
|
|
160
|
+
registry.invalidate(name);
|
|
161
|
+
return {
|
|
162
|
+
doctype: name,
|
|
163
|
+
table: getTableName(name),
|
|
164
|
+
action: 'MIGRATED',
|
|
165
|
+
version: nextVersion,
|
|
166
|
+
changes_applied: ddlExecuted
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
return c.json(output);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
if (err.message.includes('NotFound:')) {
|
|
173
|
+
return c.json({ error: err.message }, 404);
|
|
174
|
+
}
|
|
175
|
+
console.error('Doctype update failed:', err);
|
|
176
|
+
return c.json({ error: 'Failed to update database metadata', details: err.message }, 500);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
/**
|
|
180
|
+
* GET /api/meta/doctypes/:name/history
|
|
181
|
+
* Retrieve historical configuration log.
|
|
182
|
+
*/
|
|
183
|
+
metaRouter.get('/doctypes/:name/history', async (c) => {
|
|
184
|
+
const name = c.req.param('name');
|
|
185
|
+
try {
|
|
186
|
+
const res = await query(`SELECT version, changed_by, changed_at, notes, ddl_applied, definition
|
|
187
|
+
FROM _doctype_definition_history
|
|
188
|
+
WHERE doctype_name = $1
|
|
189
|
+
ORDER BY version DESC`, [name]);
|
|
190
|
+
return c.json(res.rows);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
return c.json({ error: 'Failed to fetch metadata history', details: err.message }, 500);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
/**
|
|
197
|
+
* POST /api/meta/doctypes/:name/revert
|
|
198
|
+
* Revert doctype metadata schema and trigger safe migrations to target version.
|
|
199
|
+
*/
|
|
200
|
+
metaRouter.post('/doctypes/:name/revert', async (c) => {
|
|
201
|
+
const name = c.req.param('name');
|
|
202
|
+
const body = await c.req.json();
|
|
203
|
+
const targetVersion = body.version;
|
|
204
|
+
if (typeof targetVersion !== 'number') {
|
|
205
|
+
return c.json({ error: 'Missing or invalid version number in body' }, 400);
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const output = await withTransaction(async (client) => {
|
|
209
|
+
// 1. Retrieve definition from history
|
|
210
|
+
const historyRes = await client.query(`SELECT definition FROM _doctype_definition_history
|
|
211
|
+
WHERE doctype_name = $1 AND version = $2`, [name, targetVersion]);
|
|
212
|
+
if (historyRes.rows.length === 0) {
|
|
213
|
+
throw new Error(`NotFound: Version ${targetVersion} for DocType '${name}' was not found.`);
|
|
214
|
+
}
|
|
215
|
+
const targetDefinition = historyRes.rows[0].definition;
|
|
216
|
+
// 2. Compute next version number
|
|
217
|
+
const versionRes = await client.query('SELECT COALESCE(MAX(version), 0) + 1 AS next FROM _doctype_definition_history WHERE doctype_name = $1', [name]);
|
|
218
|
+
const nextVersion = versionRes.rows[0].next;
|
|
219
|
+
// 3. Perform dynamic schema synchronization across all tenant databases
|
|
220
|
+
const ddlExecuted = await syncDatabaseSchema(client, targetDefinition);
|
|
221
|
+
// 4. Update active registry
|
|
222
|
+
await client.query(`UPDATE _doctype_meta SET definition = $2, updated_at = NOW() WHERE name = $1`, [name, JSON.stringify(targetDefinition)]);
|
|
223
|
+
// 5. Log migration history
|
|
224
|
+
if (ddlExecuted.length > 0) {
|
|
225
|
+
await client.query(`INSERT INTO _doctype_migration_history (doctype_name, version, ddl_executed, executed_by)
|
|
226
|
+
VALUES ($1, $2, $3, $4)`, [name, nextVersion, ddlExecuted.join('\n'), 'System Admin']);
|
|
227
|
+
}
|
|
228
|
+
// 6. Log version history tracking
|
|
229
|
+
await client.query(`INSERT INTO _doctype_definition_history (doctype_name, version, definition, changed_by, ddl_applied, notes)
|
|
230
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [
|
|
231
|
+
name,
|
|
232
|
+
nextVersion,
|
|
233
|
+
JSON.stringify(targetDefinition),
|
|
234
|
+
'System Admin',
|
|
235
|
+
ddlExecuted.join('\n'),
|
|
236
|
+
`Reverted active schema to version ${targetVersion}`
|
|
237
|
+
]);
|
|
238
|
+
// Invalidate cache
|
|
239
|
+
registry.invalidate(name);
|
|
240
|
+
return {
|
|
241
|
+
doctype: name,
|
|
242
|
+
table: getTableName(name),
|
|
243
|
+
action: 'REVERTED',
|
|
244
|
+
version: nextVersion,
|
|
245
|
+
changes_applied: ddlExecuted
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
return c.json(output);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
if (err.message.includes('NotFound:')) {
|
|
252
|
+
return c.json({ error: err.message }, 404);
|
|
253
|
+
}
|
|
254
|
+
console.error('Revert failed:', err);
|
|
255
|
+
return c.json({ error: 'Failed to revert metadata schema', details: err.message }, 500);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
/**
|
|
259
|
+
* POST /api/meta/tenants
|
|
260
|
+
* Register a new tenant, create its Postgres schema, and migrate all existing definitions.
|
|
261
|
+
*/
|
|
262
|
+
metaRouter.post('/tenants', async (c) => {
|
|
263
|
+
const body = await c.req.json();
|
|
264
|
+
const { id, name, domain } = body;
|
|
265
|
+
if (!id || !name) {
|
|
266
|
+
return c.json({ error: 'Tenant id and name are required' }, 400);
|
|
267
|
+
}
|
|
268
|
+
const TENANT_REGEX = /^[a-zA-Z0-9_]{1,63}$/;
|
|
269
|
+
if (!TENANT_REGEX.test(id)) {
|
|
270
|
+
return c.json({ error: 'Invalid Tenant ID format' }, 400);
|
|
271
|
+
}
|
|
272
|
+
const tenantId = id.toLowerCase().trim();
|
|
273
|
+
try {
|
|
274
|
+
const output = await withTransaction(async (client) => {
|
|
275
|
+
// 1. Check if tenant already exists
|
|
276
|
+
const checkRes = await client.query('SELECT 1 FROM _tenants WHERE id = $1', [tenantId]);
|
|
277
|
+
if (checkRes.rows.length > 0) {
|
|
278
|
+
throw new Error(`Conflict: Tenant '${tenantId}' already exists.`);
|
|
279
|
+
}
|
|
280
|
+
// 2. Insert tenant record into central _tenants table
|
|
281
|
+
await client.query(`INSERT INTO _tenants (id, name, domain) VALUES ($1, $2, $3)`, [tenantId, name, domain || null]);
|
|
282
|
+
// 3. Create the schema for the tenant
|
|
283
|
+
await client.query(`CREATE SCHEMA IF NOT EXISTS ${tenantId}`);
|
|
284
|
+
// 4. Retrieve all current doctypes to copy schemas to the new tenant schema
|
|
285
|
+
const doctypesRes = await client.query('SELECT definition FROM _doctype_meta');
|
|
286
|
+
const doctypes = doctypesRes.rows.map((row) => row.definition);
|
|
287
|
+
const createdTables = [];
|
|
288
|
+
// Temporarily swap search_path to the new tenant schema to run table creations
|
|
289
|
+
await client.query(`SET search_path TO ${tenantId}, public`);
|
|
290
|
+
for (const doc of doctypes) {
|
|
291
|
+
const ddl = generateCreateTableDDL(doc);
|
|
292
|
+
for (const sql of ddl) {
|
|
293
|
+
await client.query(sql);
|
|
294
|
+
}
|
|
295
|
+
createdTables.push(doc.name);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
tenantId,
|
|
299
|
+
name,
|
|
300
|
+
domain,
|
|
301
|
+
createdTables
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
return c.json(output);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if (err.message.includes('Conflict:')) {
|
|
308
|
+
return c.json({ error: err.message }, 409);
|
|
309
|
+
}
|
|
310
|
+
console.error('Tenant registration failed:', err);
|
|
311
|
+
return c.json({ error: 'Failed to register tenant', details: err.message }, 500);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
/**
|
|
315
|
+
* GET /api/meta/tenants
|
|
316
|
+
* List all registered tenants.
|
|
317
|
+
*/
|
|
318
|
+
metaRouter.get('/tenants', async (c) => {
|
|
319
|
+
try {
|
|
320
|
+
const res = await query('SELECT id, name, domain, created_at FROM _tenants ORDER BY id ASC');
|
|
321
|
+
return c.json(res.rows);
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
return c.json({ error: 'Failed to retrieve tenants', details: err.message }, 500);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
/**
|
|
328
|
+
* GET /api/meta/doctypes
|
|
329
|
+
* List all definitions.
|
|
330
|
+
*/
|
|
331
|
+
metaRouter.get('/doctypes', async (c) => {
|
|
332
|
+
try {
|
|
333
|
+
const res = await query('SELECT name, created_at, updated_at FROM _doctype_meta ORDER BY name ASC');
|
|
334
|
+
return c.json(res.rows);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
return c.json({ error: 'Failed to retrieve doctypes list', details: err.message }, 500);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
/**
|
|
341
|
+
* GET /api/meta/doctypes/:name
|
|
342
|
+
* Retrieve specific definition metadata.
|
|
343
|
+
*/
|
|
344
|
+
metaRouter.get('/doctypes/:name', async (c) => {
|
|
345
|
+
const name = c.req.param('name');
|
|
346
|
+
const doc = await registry.get(name);
|
|
347
|
+
if (!doc) {
|
|
348
|
+
return c.json({ error: `DocType '${name}' not found` }, 404);
|
|
349
|
+
}
|
|
350
|
+
return c.json(doc);
|
|
351
|
+
});
|
|
352
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","sourceRoot":"","sources":["meta.ts"],"names":[],"mappings":"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"}
|