@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.
Files changed (158) hide show
  1. package/dist/config.d.ts +22 -0
  2. package/dist/config.d.ts.map +1 -0
  3. package/dist/config.js +34 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/context.d.ts +7 -0
  6. package/dist/context.d.ts.map +1 -0
  7. package/dist/context.js +3 -0
  8. package/dist/context.js.map +1 -0
  9. package/dist/controllers/index.d.ts +39 -0
  10. package/dist/controllers/index.d.ts.map +1 -0
  11. package/dist/controllers/index.js +39 -0
  12. package/dist/controllers/index.js.map +1 -0
  13. package/dist/db.d.ts +6 -0
  14. package/dist/db.d.ts.map +1 -0
  15. package/dist/db.js +74 -0
  16. package/dist/db.js.map +1 -0
  17. package/dist/index.d.ts +17 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +154 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/middleware/auth.d.ts +15 -0
  22. package/dist/middleware/auth.d.ts.map +1 -0
  23. package/dist/middleware/auth.js +93 -0
  24. package/dist/middleware/auth.js.map +1 -0
  25. package/dist/middleware/body-limit.d.ts +9 -0
  26. package/dist/middleware/body-limit.d.ts.map +1 -0
  27. package/dist/middleware/body-limit.js +15 -0
  28. package/dist/middleware/body-limit.js.map +1 -0
  29. package/dist/middleware/rate-limit.d.ts +6 -0
  30. package/dist/middleware/rate-limit.d.ts.map +1 -0
  31. package/dist/middleware/rate-limit.js +40 -0
  32. package/dist/middleware/rate-limit.js.map +1 -0
  33. package/dist/middleware/rbac.d.ts +10 -0
  34. package/dist/middleware/rbac.d.ts.map +1 -0
  35. package/dist/middleware/rbac.js +61 -0
  36. package/dist/middleware/rbac.js.map +1 -0
  37. package/dist/middleware/tenant.d.ts +3 -0
  38. package/dist/middleware/tenant.d.ts.map +1 -0
  39. package/dist/middleware/tenant.js +19 -0
  40. package/dist/middleware/tenant.js.map +1 -0
  41. package/dist/registry.d.ts +26 -0
  42. package/dist/registry.d.ts.map +1 -0
  43. package/dist/registry.js +112 -0
  44. package/dist/registry.js.map +1 -0
  45. package/dist/routes/auth.d.ts +3 -0
  46. package/dist/routes/auth.d.ts.map +1 -0
  47. package/dist/routes/auth.js +141 -0
  48. package/dist/routes/auth.js.map +1 -0
  49. package/dist/routes/crud.d.ts +7 -0
  50. package/dist/routes/crud.d.ts.map +1 -0
  51. package/dist/routes/crud.js +845 -0
  52. package/dist/routes/crud.js.map +1 -0
  53. package/dist/routes/files.d.ts +7 -0
  54. package/dist/routes/files.d.ts.map +1 -0
  55. package/dist/routes/files.js +123 -0
  56. package/dist/routes/files.js.map +1 -0
  57. package/dist/routes/meta.d.ts +3 -0
  58. package/dist/routes/meta.d.ts.map +1 -0
  59. package/dist/routes/meta.js +352 -0
  60. package/dist/routes/meta.js.map +1 -0
  61. package/dist/scheduler.d.ts +33 -0
  62. package/dist/scheduler.d.ts.map +1 -0
  63. package/dist/scheduler.js +97 -0
  64. package/dist/scheduler.js.map +1 -0
  65. package/dist/utils/link-validator.d.ts +7 -0
  66. package/dist/utils/link-validator.d.ts.map +1 -0
  67. package/dist/utils/link-validator.js +33 -0
  68. package/dist/utils/link-validator.js.map +1 -0
  69. package/dist/utils/resolver.d.ts +5 -0
  70. package/dist/utils/resolver.d.ts.map +1 -0
  71. package/dist/utils/resolver.js +58 -0
  72. package/dist/utils/resolver.js.map +1 -0
  73. package/package.json +24 -0
  74. package/src/api.test.ts +362 -0
  75. package/src/config.d.ts +22 -0
  76. package/src/config.d.ts.map +1 -0
  77. package/src/config.js +34 -0
  78. package/src/config.js.map +1 -0
  79. package/src/config.ts +38 -0
  80. package/src/context.d.ts +7 -0
  81. package/src/context.d.ts.map +1 -0
  82. package/src/context.js +3 -0
  83. package/src/context.js.map +1 -0
  84. package/src/context.ts +8 -0
  85. package/src/controllers/index.d.ts +39 -0
  86. package/src/controllers/index.d.ts.map +1 -0
  87. package/src/controllers/index.js +39 -0
  88. package/src/controllers/index.js.map +1 -0
  89. package/src/controllers/index.ts +51 -0
  90. package/src/db.d.ts +6 -0
  91. package/src/db.d.ts.map +1 -0
  92. package/src/db.js +74 -0
  93. package/src/db.js.map +1 -0
  94. package/src/db.ts +73 -0
  95. package/src/index.ts +178 -0
  96. package/src/integration.test.ts +453 -0
  97. package/src/middleware/auth.d.ts +15 -0
  98. package/src/middleware/auth.d.ts.map +1 -0
  99. package/src/middleware/auth.js +93 -0
  100. package/src/middleware/auth.js.map +1 -0
  101. package/src/middleware/auth.ts +109 -0
  102. package/src/middleware/body-limit.d.ts +9 -0
  103. package/src/middleware/body-limit.d.ts.map +1 -0
  104. package/src/middleware/body-limit.js +15 -0
  105. package/src/middleware/body-limit.js.map +1 -0
  106. package/src/middleware/body-limit.ts +16 -0
  107. package/src/middleware/rate-limit.d.ts +6 -0
  108. package/src/middleware/rate-limit.d.ts.map +1 -0
  109. package/src/middleware/rate-limit.js +40 -0
  110. package/src/middleware/rate-limit.js.map +1 -0
  111. package/src/middleware/rate-limit.ts +47 -0
  112. package/src/middleware/rbac.d.ts +10 -0
  113. package/src/middleware/rbac.d.ts.map +1 -0
  114. package/src/middleware/rbac.js +61 -0
  115. package/src/middleware/rbac.js.map +1 -0
  116. package/src/middleware/rbac.ts +71 -0
  117. package/src/middleware/tenant.d.ts +3 -0
  118. package/src/middleware/tenant.d.ts.map +1 -0
  119. package/src/middleware/tenant.js +19 -0
  120. package/src/middleware/tenant.js.map +1 -0
  121. package/src/middleware/tenant.ts +24 -0
  122. package/src/registry.d.ts +26 -0
  123. package/src/registry.d.ts.map +1 -0
  124. package/src/registry.js +112 -0
  125. package/src/registry.js.map +1 -0
  126. package/src/registry.ts +123 -0
  127. package/src/routes/auth.d.ts +3 -0
  128. package/src/routes/auth.d.ts.map +1 -0
  129. package/src/routes/auth.js +141 -0
  130. package/src/routes/auth.js.map +1 -0
  131. package/src/routes/auth.ts +164 -0
  132. package/src/routes/crud.d.ts +7 -0
  133. package/src/routes/crud.d.ts.map +1 -0
  134. package/src/routes/crud.js +845 -0
  135. package/src/routes/crud.js.map +1 -0
  136. package/src/routes/crud.ts +1029 -0
  137. package/src/routes/files.d.ts +7 -0
  138. package/src/routes/files.d.ts.map +1 -0
  139. package/src/routes/files.js +123 -0
  140. package/src/routes/files.js.map +1 -0
  141. package/src/routes/files.ts +143 -0
  142. package/src/routes/meta.d.ts +3 -0
  143. package/src/routes/meta.d.ts.map +1 -0
  144. package/src/routes/meta.js +352 -0
  145. package/src/routes/meta.js.map +1 -0
  146. package/src/routes/meta.ts +448 -0
  147. package/src/scheduler.ts +118 -0
  148. package/src/utils/link-validator.d.ts +7 -0
  149. package/src/utils/link-validator.d.ts.map +1 -0
  150. package/src/utils/link-validator.js +33 -0
  151. package/src/utils/link-validator.js.map +1 -0
  152. package/src/utils/link-validator.ts +45 -0
  153. package/src/utils/resolver.d.ts +5 -0
  154. package/src/utils/resolver.d.ts.map +1 -0
  155. package/src/utils/resolver.js +58 -0
  156. package/src/utils/resolver.js.map +1 -0
  157. package/src/utils/resolver.ts +65 -0
  158. 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":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAQ5B;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,CAAU,EAAE,IAAU;IACzD,MAAM,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAEjD,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,kEAAkE;QAClE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE;YACZ,KAAK,EAAE,oBAAoB;YAC3B,KAAK,EAAE,CAAC,OAAO,CAAC;YAChB,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE;SACjB,CAAC,CAAC;QAClB,MAAM,IAAI,EAAE,CAAC;QACb,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAClC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,kCAAkC,EAAE,EAAE,GAAG,CAAC,CAAC;IACpE,CAAC;IAED,gDAAgD;IAChD,IAAI,KAAK,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QACxB,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oDAAoD,EAAE,EAAE,GAAG,CAAC,CAAC;IACtF,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAE/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;YAEzC,IAAI,SAAS,EAAE,CAAC;gBACd,2CAA2C;gBAC3C,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC;gBACpD,MAAM,cAAc,GAAG,GAAG,SAAS,IAAI,UAAU,EAAE,CAAC;gBACpD,MAAM,iBAAiB,GAAG,MAAM;qBAC7B,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC;qBAC/B,MAAM,CAAC,cAAc,CAAC;qBACtB,MAAM,CAAC,WAAW,CAAC,CAAC;gBAEvB,IAAI,YAAY,KAAK,iBAAiB,EAAE,CAAC;oBACvC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uCAAuC,EAAE,EAAE,GAAG,CAAC,CAAC;gBACzE,CAAC;gBAED,8BAA8B;gBAC9B,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;gBAC9G,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;gBAExC,qBAAqB;gBACrB,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;oBACnD,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iCAAiC,EAAE,EAAE,GAAG,CAAC,CAAC;gBACnE,CAAC;gBAED,mEAAmE;gBACnE,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,EAAE,CAAC;oBACxD,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0CAA0C,EAAE,EAAE,GAAG,CAAC,CAAC;gBAC5E,CAAC;gBAED,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE;oBACZ,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,oBAAoB;oBAC5C,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;oBAC/D,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE;iBACjB,CAAC,CAAC;YACpB,CAAC;iBAAM,CAAC;gBACN,yEAAyE;gBACzE,OAAO,CAAC,IAAI,CAAC,0FAA0F,CAAC,CAAC;gBACzG,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBAClE,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;gBACxE,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;gBAExC,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE;oBACZ,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,oBAAoB;oBAC5C,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;oBAC/D,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE;iBACjB,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;aAAM,CAAC;YACN,8CAA8C;YAC9C,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;gBAC1C,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE,EAAE,GAAG,CAAC,CAAC;YACtE,CAAC;YAED,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;gBAC5B,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAAC,gBAAgB,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,EAAiB,CAAC,CAAC;YAC5H,CAAC;iBAAM,IAAI,KAAK,KAAK,YAAY,EAAE,CAAC;gBAClC,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,CAAC,UAAU,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,EAAiB,CAAC,CAAC;YACrH,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gDAAgD,EAAE,EAAE,GAAG,CAAC,CAAC;YAClF,CAAC;QACH,CAAC;QACD,MAAM,IAAI,EAAE,CAAC;IACf,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,+CAA+C,EAAE,EAAE,GAAG,CAAC,CAAC;IACjF,CAAC;AACH,CAAC"}