@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,453 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { query } from './db.js';
|
|
4
|
+
import { registry } from './registry.js';
|
|
5
|
+
import { generateCreateTableDDL } from '@aruvili/core';
|
|
6
|
+
import { crudRouter } from './routes/crud.js';
|
|
7
|
+
import { metaRouter } from './routes/meta.js';
|
|
8
|
+
import { tenantMiddleware } from './middleware/tenant.js';
|
|
9
|
+
import { DocTypeDefinition } from '@aruvili/core';
|
|
10
|
+
|
|
11
|
+
describe('Real PostgreSQL Integration Tests', () => {
|
|
12
|
+
const app = new Hono<any>();
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
(globalThis as any).dbMock = undefined;
|
|
16
|
+
// 1. Ensure system tables exist
|
|
17
|
+
await query(`CREATE TABLE IF NOT EXISTS _tenants (
|
|
18
|
+
id VARCHAR(100) PRIMARY KEY,
|
|
19
|
+
name VARCHAR(255) NOT NULL,
|
|
20
|
+
domain VARCHAR(255),
|
|
21
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
22
|
+
);`);
|
|
23
|
+
|
|
24
|
+
await query(`CREATE TABLE IF NOT EXISTS _doctype_meta (
|
|
25
|
+
name VARCHAR(255) PRIMARY KEY,
|
|
26
|
+
definition JSONB NOT NULL,
|
|
27
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
28
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
29
|
+
);`);
|
|
30
|
+
|
|
31
|
+
await query(`CREATE TABLE IF NOT EXISTS _naming_series (
|
|
32
|
+
prefix VARCHAR(100) PRIMARY KEY,
|
|
33
|
+
current_value INTEGER NOT NULL DEFAULT 0
|
|
34
|
+
);`);
|
|
35
|
+
|
|
36
|
+
await query(`CREATE TABLE IF NOT EXISTS _doctype_migration_history (
|
|
37
|
+
id SERIAL PRIMARY KEY, uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
|
|
38
|
+
doctype_name VARCHAR(255) NOT NULL, version INTEGER NOT NULL,
|
|
39
|
+
ddl_executed TEXT NOT NULL, executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
40
|
+
executed_by VARCHAR(255)
|
|
41
|
+
);`);
|
|
42
|
+
|
|
43
|
+
await query(`CREATE TABLE IF NOT EXISTS _doctype_definition_history (
|
|
44
|
+
id SERIAL PRIMARY KEY,
|
|
45
|
+
doctype_name VARCHAR(255) REFERENCES _doctype_meta(name) ON DELETE CASCADE,
|
|
46
|
+
version INTEGER NOT NULL,
|
|
47
|
+
definition JSONB NOT NULL,
|
|
48
|
+
changed_by VARCHAR(255) NOT NULL,
|
|
49
|
+
changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
50
|
+
ddl_applied TEXT,
|
|
51
|
+
notes TEXT
|
|
52
|
+
);`);
|
|
53
|
+
|
|
54
|
+
await query(`CREATE TABLE IF NOT EXISTS _audit_log (
|
|
55
|
+
id BIGSERIAL PRIMARY KEY,
|
|
56
|
+
doctype_name VARCHAR(255) NOT NULL,
|
|
57
|
+
docname VARCHAR(255) NOT NULL,
|
|
58
|
+
action VARCHAR(50) NOT NULL,
|
|
59
|
+
changed_by VARCHAR(255) NOT NULL,
|
|
60
|
+
changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
61
|
+
diff JSONB NOT NULL
|
|
62
|
+
);`);
|
|
63
|
+
|
|
64
|
+
// Setup app route and mock user context
|
|
65
|
+
app.use('*', tenantMiddleware);
|
|
66
|
+
app.use('*', async (c, next) => {
|
|
67
|
+
c.set('user', { email: 'admin@aruvili.com', roles: ['System Manager'] });
|
|
68
|
+
await next();
|
|
69
|
+
});
|
|
70
|
+
app.route('/api/doctype/:doctype', crudRouter);
|
|
71
|
+
app.route('/api/meta', metaRouter);
|
|
72
|
+
|
|
73
|
+
// Clean any residual test state
|
|
74
|
+
await query(`DROP SCHEMA IF EXISTS tenant_alpha CASCADE;`);
|
|
75
|
+
await query(`DELETE FROM _tenants WHERE id = 'tenant_alpha';`);
|
|
76
|
+
await query(`DROP TABLE IF EXISTS dt_testtask CASCADE;`);
|
|
77
|
+
await query(`DROP TABLE IF EXISTS dt_testproject CASCADE;`);
|
|
78
|
+
await query(`DROP TABLE IF EXISTS dt_testschemahistory CASCADE;`);
|
|
79
|
+
await query(`DROP TABLE IF EXISTS dt_testmultitenantpost CASCADE;`);
|
|
80
|
+
await query(`DELETE FROM _doctype_meta WHERE name IN ('TestTask', 'TestProject', 'TestSchemaHistory', 'TestMultiTenantPost');`);
|
|
81
|
+
await query(`DELETE FROM _naming_series WHERE prefix = 'TSK-';`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterAll(async () => {
|
|
85
|
+
// Cleanup created test tables
|
|
86
|
+
await query(`DROP SCHEMA IF EXISTS tenant_alpha CASCADE;`);
|
|
87
|
+
await query(`DELETE FROM _tenants WHERE id = 'tenant_alpha';`);
|
|
88
|
+
await query(`DROP TABLE IF EXISTS dt_testtask CASCADE;`);
|
|
89
|
+
await query(`DROP TABLE IF EXISTS dt_testproject CASCADE;`);
|
|
90
|
+
await query(`DROP TABLE IF EXISTS dt_testschemahistory CASCADE;`);
|
|
91
|
+
await query(`DROP TABLE IF EXISTS dt_testmultitenantpost CASCADE;`);
|
|
92
|
+
await query(`DELETE FROM _doctype_meta WHERE name IN ('TestTask', 'TestProject', 'TestSchemaHistory', 'TestMultiTenantPost');`);
|
|
93
|
+
await query(`DELETE FROM _naming_series WHERE prefix = 'TSK-';`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should dynamically migrate, validate, and execute operations against real PostgreSQL tables', async () => {
|
|
97
|
+
// 1. Define TestProject (parent table)
|
|
98
|
+
const testProjectSchema: DocTypeDefinition = {
|
|
99
|
+
name: 'TestProject',
|
|
100
|
+
fields: [
|
|
101
|
+
{ fieldname: 'project_name', label: 'Project Name', fieldtype: 'Text', required: true }
|
|
102
|
+
],
|
|
103
|
+
permissions: [
|
|
104
|
+
{ role: 'System Manager', create: true, read: true, update: true, delete: true }
|
|
105
|
+
]
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// 2. Define TestTask (child table linked to TestProject, supporting versioning & workflow)
|
|
109
|
+
const testTaskSchema: DocTypeDefinition = {
|
|
110
|
+
name: 'TestTask',
|
|
111
|
+
naming_rule: 'NamingSeries',
|
|
112
|
+
naming_series: 'TSK-.#####',
|
|
113
|
+
fields: [
|
|
114
|
+
{ fieldname: 'title', label: 'Title', fieldtype: 'Text', required: true },
|
|
115
|
+
{ fieldname: 'project', label: 'Project Link', fieldtype: 'Link', options: 'TestProject' },
|
|
116
|
+
{ fieldname: 'status', label: 'Status', fieldtype: 'Select', options: 'Draft,In Progress,Done' }
|
|
117
|
+
],
|
|
118
|
+
permissions: [
|
|
119
|
+
{ role: 'System Manager', create: true, read: true, update: true, delete: true }
|
|
120
|
+
],
|
|
121
|
+
workflow: {
|
|
122
|
+
fieldname: 'status',
|
|
123
|
+
initial_state: 'Draft',
|
|
124
|
+
transitions: [
|
|
125
|
+
{ state: 'Draft', action: 'Start', next_state: 'In Progress', allowed_roles: ['System Manager'] },
|
|
126
|
+
{ state: 'In Progress', action: 'Complete', next_state: 'Done', allowed_roles: ['System Manager'] }
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Save definitions to _doctype_meta
|
|
132
|
+
await query(
|
|
133
|
+
`INSERT INTO _doctype_meta (name, definition) VALUES ($1, $2), ($3, $4)`,
|
|
134
|
+
['TestProject', JSON.stringify(testProjectSchema), 'TestTask', JSON.stringify(testTaskSchema)]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Invalidate cache in registry
|
|
138
|
+
registry.invalidate('TestProject');
|
|
139
|
+
registry.invalidate('TestTask');
|
|
140
|
+
|
|
141
|
+
// Run migrations using generated DDLs
|
|
142
|
+
const projectDDLs = generateCreateTableDDL(testProjectSchema);
|
|
143
|
+
for (const statement of projectDDLs) {
|
|
144
|
+
await query(statement);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const taskDDLs = generateCreateTableDDL(testTaskSchema);
|
|
148
|
+
for (const statement of taskDDLs) {
|
|
149
|
+
await query(statement);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Test insert parent document via HTTP router
|
|
153
|
+
const projectRes = await app.request('/api/doctype/TestProject', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
156
|
+
body: JSON.stringify({ name: 'PROJ-001', project_name: 'Aruvili Hardening' })
|
|
157
|
+
});
|
|
158
|
+
expect(projectRes.status).toBe(200);
|
|
159
|
+
|
|
160
|
+
// 4. Test link validation check: inserting task with wrong link must fail
|
|
161
|
+
const invalidTaskRes = await app.request('/api/doctype/TestTask', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
body: JSON.stringify({ title: 'Task with invalid link', project: 'NON_EXISTENT_PROJ' })
|
|
165
|
+
});
|
|
166
|
+
expect(invalidTaskRes.status).toBe(400);
|
|
167
|
+
const invalidJson = await invalidTaskRes.json();
|
|
168
|
+
expect(invalidJson.details).toContain('Link validation failed');
|
|
169
|
+
|
|
170
|
+
// 5. Insert task with valid link (naming series should generate TSK-00001)
|
|
171
|
+
const validTaskRes = await app.request('/api/doctype/TestTask', {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ title: 'Dynamic Task', project: 'PROJ-001' })
|
|
175
|
+
});
|
|
176
|
+
expect(validTaskRes.status).toBe(200);
|
|
177
|
+
const taskData = await validTaskRes.json();
|
|
178
|
+
expect(taskData.name).toBe('TSK-00001');
|
|
179
|
+
expect(taskData.version).toBe(1);
|
|
180
|
+
expect(taskData.status).toBe('Draft');
|
|
181
|
+
|
|
182
|
+
// 6. Test Optimistic Concurrency Control (OCC)
|
|
183
|
+
// Update task to version 2
|
|
184
|
+
const updateRes = await app.request(`/api/doctype/TestTask/TSK-00001`, {
|
|
185
|
+
method: 'PUT',
|
|
186
|
+
headers: { 'Content-Type': 'application/json' },
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
title: 'Updated Task Name',
|
|
189
|
+
version: 1
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
expect(updateRes.status).toBe(200);
|
|
193
|
+
const updatedData = await updateRes.json();
|
|
194
|
+
expect(updatedData.version).toBe(2);
|
|
195
|
+
|
|
196
|
+
// Trigger update with obsolete version (must fail)
|
|
197
|
+
const staleUpdateRes = await app.request(`/api/doctype/TestTask/TSK-00001`, {
|
|
198
|
+
method: 'PUT',
|
|
199
|
+
headers: { 'Content-Type': 'application/json' },
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
title: 'Obsolete update description',
|
|
202
|
+
version: 1
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
expect(staleUpdateRes.status).toBe(409); // Conflict
|
|
206
|
+
|
|
207
|
+
// 7. Test Workflow transition via route
|
|
208
|
+
const workflowRes = await app.request(`/api/doctype/TestTask/TSK-00001/workflow`, {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: { 'Content-Type': 'application/json' },
|
|
211
|
+
body: JSON.stringify({ action: 'Start' })
|
|
212
|
+
});
|
|
213
|
+
expect(workflowRes.status).toBe(200);
|
|
214
|
+
const workflowData = await workflowRes.json();
|
|
215
|
+
expect(workflowData.status).toBe('In Progress');
|
|
216
|
+
|
|
217
|
+
// 8. Test Bulk Operations Rollback on error
|
|
218
|
+
// Insert a batch where 1st is valid, 2nd is invalid (fails link check)
|
|
219
|
+
const bulkRes = await app.request('/api/doctype/TestTask/bulk', {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
action: 'insert',
|
|
224
|
+
items: [
|
|
225
|
+
{ title: 'Valid Bulk Task', project: 'PROJ-001' },
|
|
226
|
+
{ title: 'Invalid Bulk Task', project: 'INVALID_PROJECT_NAME' }
|
|
227
|
+
]
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
expect(bulkRes.status).toBe(500); // Transaction rolled back
|
|
231
|
+
const bulkJson = await bulkRes.json();
|
|
232
|
+
expect(bulkJson.error).toContain('Bulk transaction rolled back');
|
|
233
|
+
|
|
234
|
+
// Check that 'Valid Bulk Task' was NOT written to the DB (verifying atomicity)
|
|
235
|
+
const checkDbRes = await query(`SELECT * FROM dt_testtask WHERE title = 'Valid Bulk Task'`);
|
|
236
|
+
expect(checkDbRes.rows.length).toBe(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should run background tasks and enforce distributed locks across tick cycles', async () => {
|
|
240
|
+
const { scheduler } = await import('./scheduler.js');
|
|
241
|
+
let runCount = 0;
|
|
242
|
+
|
|
243
|
+
scheduler.register({
|
|
244
|
+
name: 'integration-test-task',
|
|
245
|
+
intervalSeconds: 5,
|
|
246
|
+
execute: async (client) => {
|
|
247
|
+
runCount++;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Make sure scheduler table is initialized
|
|
252
|
+
await scheduler.start(999999);
|
|
253
|
+
scheduler.stop(); // Stop automatic interval tick
|
|
254
|
+
|
|
255
|
+
// Clear any leftover lock records from old test runs
|
|
256
|
+
await query(`DELETE FROM _scheduled_jobs WHERE name = 'integration-test-task'`);
|
|
257
|
+
|
|
258
|
+
// Tick 1: Should run because it hasn't started yet
|
|
259
|
+
await (scheduler as any).tick();
|
|
260
|
+
expect(runCount).toBe(1);
|
|
261
|
+
|
|
262
|
+
// Verify lock record in DB
|
|
263
|
+
const jobRes = await query(`SELECT status FROM _scheduled_jobs WHERE name = 'integration-test-task'`);
|
|
264
|
+
expect(jobRes.rows[0].status).toBe('SUCCESS');
|
|
265
|
+
|
|
266
|
+
// Tick 2: Should NOT run because interval (5s) has not passed yet
|
|
267
|
+
await (scheduler as any).tick();
|
|
268
|
+
expect(runCount).toBe(1); // Should still be 1
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should support registering doctypes, handling naming conflicts, tracking history, and reverting schemas', async () => {
|
|
272
|
+
// 1. Create a new DocType via API
|
|
273
|
+
const schemaDef = {
|
|
274
|
+
name: 'TestSchemaHistory',
|
|
275
|
+
fields: [
|
|
276
|
+
{ fieldname: 'title', label: 'Title', fieldtype: 'Text', required: true }
|
|
277
|
+
],
|
|
278
|
+
permissions: [
|
|
279
|
+
{ role: 'System Manager', create: true, read: true, update: true, delete: true }
|
|
280
|
+
]
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const createRes = await app.request('/api/meta/doctypes', {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: { 'Content-Type': 'application/json' },
|
|
286
|
+
body: JSON.stringify(schemaDef)
|
|
287
|
+
});
|
|
288
|
+
expect(createRes.status).toBe(200);
|
|
289
|
+
const createData = await createRes.json();
|
|
290
|
+
expect(createData.action).toBe('CREATED');
|
|
291
|
+
expect(createData.version).toBe(1);
|
|
292
|
+
|
|
293
|
+
// 2. Test duplicate conflict: registering duplicate name must fail with 409
|
|
294
|
+
const conflictRes = await app.request('/api/meta/doctypes', {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify(schemaDef)
|
|
298
|
+
});
|
|
299
|
+
expect(conflictRes.status).toBe(409);
|
|
300
|
+
const conflictJson = await conflictRes.json();
|
|
301
|
+
expect(conflictJson.error).toContain('already exists');
|
|
302
|
+
|
|
303
|
+
// 3. Update the definition (version 2) using PUT
|
|
304
|
+
const updatedSchemaDef = {
|
|
305
|
+
...schemaDef,
|
|
306
|
+
fields: [
|
|
307
|
+
{ fieldname: 'title', label: 'Title', fieldtype: 'Text', required: true },
|
|
308
|
+
{ fieldname: 'description', label: 'Description', fieldtype: 'Text' }
|
|
309
|
+
]
|
|
310
|
+
};
|
|
311
|
+
const updateRes = await app.request('/api/meta/doctypes/TestSchemaHistory', {
|
|
312
|
+
method: 'PUT',
|
|
313
|
+
headers: { 'Content-Type': 'application/json' },
|
|
314
|
+
body: JSON.stringify(updatedSchemaDef)
|
|
315
|
+
});
|
|
316
|
+
expect(updateRes.status).toBe(200);
|
|
317
|
+
const updateData = await updateRes.json();
|
|
318
|
+
expect(updateData.action).toBe('MIGRATED');
|
|
319
|
+
expect(updateData.version).toBe(2);
|
|
320
|
+
|
|
321
|
+
// 4. Check configuration history log
|
|
322
|
+
const historyRes = await app.request('/api/meta/doctypes/TestSchemaHistory/history');
|
|
323
|
+
expect(historyRes.status).toBe(200);
|
|
324
|
+
const historyData = await historyRes.json();
|
|
325
|
+
expect(historyData.length).toBe(2);
|
|
326
|
+
expect(historyData[0].version).toBe(2);
|
|
327
|
+
expect(historyData[1].version).toBe(1);
|
|
328
|
+
|
|
329
|
+
// 5. Revert schema back to version 1 (which did not have the description field)
|
|
330
|
+
const revertRes = await app.request('/api/meta/doctypes/TestSchemaHistory/revert', {
|
|
331
|
+
method: 'POST',
|
|
332
|
+
headers: { 'Content-Type': 'application/json' },
|
|
333
|
+
body: JSON.stringify({ version: 1 })
|
|
334
|
+
});
|
|
335
|
+
expect(revertRes.status).toBe(200);
|
|
336
|
+
const revertData = await revertRes.json();
|
|
337
|
+
expect(revertData.action).toBe('REVERTED');
|
|
338
|
+
expect(revertData.version).toBe(3);
|
|
339
|
+
|
|
340
|
+
// Retrieve active definition to verify that reversion is reflected in registry
|
|
341
|
+
const currentMetaRes = await app.request('/api/meta/doctypes/TestSchemaHistory');
|
|
342
|
+
expect(currentMetaRes.status).toBe(200);
|
|
343
|
+
const currentMeta = await currentMetaRes.json();
|
|
344
|
+
|
|
345
|
+
// The active definition fields should not contain the description field now
|
|
346
|
+
const fields = currentMeta.fields.map((f: any) => f.fieldname);
|
|
347
|
+
expect(fields).toContain('title');
|
|
348
|
+
expect(fields).not.toContain('description');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should isolate data using multi-tenant schemas and auto-sync schema modifications across all tenants', async () => {
|
|
352
|
+
// 1. Create a new tenant 'tenant_alpha'
|
|
353
|
+
const tenantRes = await app.request('/api/meta/tenants', {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify({ id: 'tenant_alpha', name: 'Tenant Alpha' })
|
|
357
|
+
});
|
|
358
|
+
expect(tenantRes.status).toBe(200);
|
|
359
|
+
const tenantData = await tenantRes.json();
|
|
360
|
+
expect(tenantData.tenantId).toBe('tenant_alpha');
|
|
361
|
+
|
|
362
|
+
// 2. Validate tenant list endpoint
|
|
363
|
+
const listRes = await app.request('/api/meta/tenants');
|
|
364
|
+
expect(listRes.status).toBe(200);
|
|
365
|
+
const listData = await listRes.json();
|
|
366
|
+
expect(listData.some((t: any) => t.id === 'tenant_alpha')).toBe(true);
|
|
367
|
+
|
|
368
|
+
// 3. Create document in tenant_alpha using X-Tenant-ID header
|
|
369
|
+
const createDocRes = await app.request('/api/doctype/TestSchemaHistory', {
|
|
370
|
+
method: 'POST',
|
|
371
|
+
headers: {
|
|
372
|
+
'Content-Type': 'application/json',
|
|
373
|
+
'X-Tenant-ID': 'tenant_alpha'
|
|
374
|
+
},
|
|
375
|
+
body: JSON.stringify({
|
|
376
|
+
name: 'DOC-ALPHA-01',
|
|
377
|
+
title: 'Hello from Tenant Alpha'
|
|
378
|
+
})
|
|
379
|
+
});
|
|
380
|
+
expect(createDocRes.status).toBe(200);
|
|
381
|
+
|
|
382
|
+
// 4. Verify data isolation: Get document from tenant_alpha should succeed
|
|
383
|
+
const getDocRes = await app.request('/api/doctype/TestSchemaHistory/DOC-ALPHA-01', {
|
|
384
|
+
headers: { 'X-Tenant-ID': 'tenant_alpha' }
|
|
385
|
+
});
|
|
386
|
+
expect(getDocRes.status).toBe(200);
|
|
387
|
+
const docData = await getDocRes.json();
|
|
388
|
+
expect(docData.title).toBe('Hello from Tenant Alpha');
|
|
389
|
+
|
|
390
|
+
// Get document from default schema (public) should fail/not be found
|
|
391
|
+
const getDocPublicRes = await app.request('/api/doctype/TestSchemaHistory/DOC-ALPHA-01');
|
|
392
|
+
expect(getDocPublicRes.status).toBe(404);
|
|
393
|
+
|
|
394
|
+
// 5. Test automatic schema synchronization:
|
|
395
|
+
// Create a NEW DocType definition globally
|
|
396
|
+
const newDocTypeDef = {
|
|
397
|
+
name: 'TestMultiTenantPost',
|
|
398
|
+
fields: [
|
|
399
|
+
{ fieldname: 'headline', label: 'Headline', fieldtype: 'Text', required: true }
|
|
400
|
+
],
|
|
401
|
+
permissions: [
|
|
402
|
+
{ role: 'System Manager', create: true, read: true, update: true, delete: true }
|
|
403
|
+
]
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const newDocTypeRes = await app.request('/api/meta/doctypes', {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
409
|
+
body: JSON.stringify(newDocTypeDef)
|
|
410
|
+
});
|
|
411
|
+
expect(newDocTypeRes.status).toBe(200);
|
|
412
|
+
|
|
413
|
+
// Verify we can insert record into TestMultiTenantPost in tenant_alpha
|
|
414
|
+
const insertDocAlpha = await app.request('/api/doctype/TestMultiTenantPost', {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: {
|
|
417
|
+
'Content-Type': 'application/json',
|
|
418
|
+
'X-Tenant-ID': 'tenant_alpha'
|
|
419
|
+
},
|
|
420
|
+
body: JSON.stringify({
|
|
421
|
+
name: 'POST-01',
|
|
422
|
+
headline: 'Tenant Alpha headline'
|
|
423
|
+
})
|
|
424
|
+
});
|
|
425
|
+
expect(insertDocAlpha.status).toBe(200);
|
|
426
|
+
|
|
427
|
+
// Verify we can insert record into TestMultiTenantPost in default (public) schema
|
|
428
|
+
const insertDocPublic = await app.request('/api/doctype/TestMultiTenantPost', {
|
|
429
|
+
method: 'POST',
|
|
430
|
+
headers: { 'Content-Type': 'application/json' },
|
|
431
|
+
body: JSON.stringify({
|
|
432
|
+
name: 'POST-01',
|
|
433
|
+
headline: 'Public schema headline'
|
|
434
|
+
})
|
|
435
|
+
});
|
|
436
|
+
expect(insertDocPublic.status).toBe(200);
|
|
437
|
+
|
|
438
|
+
// Verify values are isolated
|
|
439
|
+
const checkAlpha = await app.request('/api/doctype/TestMultiTenantPost/POST-01', {
|
|
440
|
+
headers: { 'X-Tenant-ID': 'tenant_alpha' }
|
|
441
|
+
});
|
|
442
|
+
const checkPublic = await app.request('/api/doctype/TestMultiTenantPost/POST-01');
|
|
443
|
+
|
|
444
|
+
expect(checkAlpha.status).toBe(200);
|
|
445
|
+
expect(checkPublic.status).toBe(200);
|
|
446
|
+
|
|
447
|
+
const dataAlpha = await checkAlpha.json();
|
|
448
|
+
const dataPublic = await checkPublic.json();
|
|
449
|
+
|
|
450
|
+
expect(dataAlpha.headline).toBe('Tenant Alpha headline');
|
|
451
|
+
expect(dataPublic.headline).toBe('Public schema headline');
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context, Next } from 'hono';
|
|
2
|
+
export interface UserSession {
|
|
3
|
+
email: string;
|
|
4
|
+
roles: string[];
|
|
5
|
+
session_id: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolves request JWT signature and binds session properties to Hono request context.
|
|
9
|
+
* In production: validates HMAC-SHA256 signature against JWT_SECRET.
|
|
10
|
+
* In development: supports mock tokens for testing.
|
|
11
|
+
*/
|
|
12
|
+
export declare function authMiddleware(c: Context, next: Next): Promise<(Response & import("hono").TypedResponse<{
|
|
13
|
+
error: string;
|
|
14
|
+
}, 401, "json">) | undefined>;
|
|
15
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAGrC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI;;8BA8F1D"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves request JWT signature and binds session properties to Hono request context.
|
|
4
|
+
* In production: validates HMAC-SHA256 signature against JWT_SECRET.
|
|
5
|
+
* In development: supports mock tokens for testing.
|
|
6
|
+
*/
|
|
7
|
+
export async function authMiddleware(c, next) {
|
|
8
|
+
const authHeader = c.req.header('Authorization');
|
|
9
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
10
|
+
// If no auth token is provided, assign Guest role session context
|
|
11
|
+
c.set('user', {
|
|
12
|
+
email: 'guest@system.local',
|
|
13
|
+
roles: ['Guest'],
|
|
14
|
+
session_id: crypto.randomUUID()
|
|
15
|
+
});
|
|
16
|
+
await next();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const token = authHeader.split(' ')[1];
|
|
20
|
+
if (!token || token.trim() === '') {
|
|
21
|
+
return c.json({ error: 'Unauthorized: Empty bearer token' }, 401);
|
|
22
|
+
}
|
|
23
|
+
// Enforce maximum token length to prevent abuse
|
|
24
|
+
if (token.length > 4096) {
|
|
25
|
+
return c.json({ error: 'Unauthorized: Token exceeds maximum allowed length' }, 401);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const parts = token.split('.');
|
|
29
|
+
if (parts.length === 3) {
|
|
30
|
+
const jwtSecret = process.env.JWT_SECRET;
|
|
31
|
+
if (jwtSecret) {
|
|
32
|
+
// Production: Verify HMAC-SHA256 signature
|
|
33
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
34
|
+
const signatureInput = `${headerB64}.${payloadB64}`;
|
|
35
|
+
const expectedSignature = crypto
|
|
36
|
+
.createHmac('sha256', jwtSecret)
|
|
37
|
+
.update(signatureInput)
|
|
38
|
+
.digest('base64url');
|
|
39
|
+
if (signatureB64 !== expectedSignature) {
|
|
40
|
+
return c.json({ error: 'Unauthorized: Invalid token signature' }, 401);
|
|
41
|
+
}
|
|
42
|
+
// Decode and validate payload
|
|
43
|
+
const payloadJson = Buffer.from(payloadB64.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf-8');
|
|
44
|
+
const payload = JSON.parse(payloadJson);
|
|
45
|
+
// Check token expiry
|
|
46
|
+
if (payload.exp && Date.now() / 1000 > payload.exp) {
|
|
47
|
+
return c.json({ error: 'Unauthorized: Token has expired' }, 401);
|
|
48
|
+
}
|
|
49
|
+
// Check issued-at is not in the future (clock skew tolerance: 60s)
|
|
50
|
+
if (payload.iat && payload.iat > Date.now() / 1000 + 60) {
|
|
51
|
+
return c.json({ error: 'Unauthorized: Token issued in the future' }, 401);
|
|
52
|
+
}
|
|
53
|
+
c.set('user', {
|
|
54
|
+
email: payload.email || 'unknown@user.local',
|
|
55
|
+
roles: Array.isArray(payload.roles) ? payload.roles : ['Guest'],
|
|
56
|
+
session_id: crypto.randomUUID()
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Development fallback: decode without verification (JWT_SECRET not set)
|
|
61
|
+
console.warn('[AUTH] JWT_SECRET not configured. Running in development mode - tokens are NOT verified.');
|
|
62
|
+
const payloadB64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
63
|
+
const payloadJson = Buffer.from(payloadB64, 'base64').toString('utf-8');
|
|
64
|
+
const payload = JSON.parse(payloadJson);
|
|
65
|
+
c.set('user', {
|
|
66
|
+
email: payload.email || 'unknown@user.local',
|
|
67
|
+
roles: Array.isArray(payload.roles) ? payload.roles : ['Guest'],
|
|
68
|
+
session_id: crypto.randomUUID()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Mock/testing token support (non-JWT format)
|
|
74
|
+
if (process.env.NODE_ENV === 'production') {
|
|
75
|
+
return c.json({ error: 'Unauthorized: Invalid token format' }, 401);
|
|
76
|
+
}
|
|
77
|
+
if (token === 'admin-token') {
|
|
78
|
+
c.set('user', { email: 'admin@system.local', roles: ['System Manager'], session_id: crypto.randomUUID() });
|
|
79
|
+
}
|
|
80
|
+
else if (token === 'user-token') {
|
|
81
|
+
c.set('user', { email: 'user@system.local', roles: ['Employee'], session_id: crypto.randomUUID() });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
return c.json({ error: 'Unauthorized: Invalid authentication signature' }, 401);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await next();
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return c.json({ error: 'Unauthorized: Could not resolve session token' }, 401);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"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"}
|