jinaga-server 3.5.2 → 3.5.4

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 (57) hide show
  1. package/dist/authorization/authorization-keystore.js +2 -2
  2. package/dist/authorization/authorization-keystore.js.map +1 -1
  3. package/dist/http/csv-metadata.d.ts +24 -0
  4. package/dist/http/csv-metadata.js +3 -0
  5. package/dist/http/csv-metadata.js.map +1 -0
  6. package/dist/http/csv-validator.d.ts +23 -0
  7. package/dist/http/csv-validator.js +83 -0
  8. package/dist/http/csv-validator.js.map +1 -0
  9. package/dist/http/output-formatters.d.ts +28 -0
  10. package/dist/http/output-formatters.js +197 -0
  11. package/dist/http/output-formatters.js.map +1 -0
  12. package/dist/http/result-stream.d.ts +35 -0
  13. package/dist/http/result-stream.js +78 -0
  14. package/dist/http/result-stream.js.map +1 -0
  15. package/dist/http/router.d.ts +7 -1
  16. package/dist/http/router.js +98 -47
  17. package/dist/http/router.js.map +1 -1
  18. package/dist/postgres/postgres-store.js +7 -5
  19. package/dist/postgres/postgres-store.js.map +1 -1
  20. package/dist/postgres/purge-sql.js +15 -2
  21. package/dist/postgres/purge-sql.js.map +1 -1
  22. package/dist/postgres/specification-result-sql.d.ts +2 -0
  23. package/dist/postgres/specification-result-sql.js +49 -5
  24. package/dist/postgres/specification-result-sql.js.map +1 -1
  25. package/dist/postgres/specification-sql.js +28 -6
  26. package/dist/postgres/specification-sql.js.map +1 -1
  27. package/docs/design/feeds-persistent-proxy-plan.md +294 -0
  28. package/integration-test/integration-test/jinaga-test/package-lock.json +6 -0
  29. package/integration-test/jinaga-test/Dockerfile +1 -6
  30. package/integration-test/jinaga-test/http-test-helpers.js +77 -0
  31. package/integration-test/jinaga-test/jinaga.test.js +9 -9
  32. package/integration-test/jinaga-test/package-lock.json +329 -32
  33. package/integration-test/jinaga-test/package.json +5 -1
  34. package/integration-test/jinaga-test/read-csv.test.js +350 -0
  35. package/integration-test/jinaga-test/read-endpoint.test.js +442 -0
  36. package/integration-test/jinaga-test/read-performance.test.js +221 -0
  37. package/integration-test/jinaga-test/read-streaming.test.js +199 -0
  38. package/integration-test/jinaga-test/specification-conditions.test.js +147 -0
  39. package/integration-test/jinaga-test/test-helpers.js +12 -0
  40. package/integration-test/jinaga-test/time-projection.test.js +235 -0
  41. package/package.json +3 -2
  42. package/src/authorization/authorization-keystore.ts +4 -4
  43. package/src/http/csv-metadata.ts +34 -0
  44. package/src/http/csv-validator.ts +90 -0
  45. package/src/http/output-formatters.ts +196 -0
  46. package/src/http/result-stream.ts +69 -0
  47. package/src/http/router.ts +117 -34
  48. package/src/postgres/postgres-store.ts +13 -6
  49. package/src/postgres/purge-sql.ts +18 -2
  50. package/src/postgres/specification-result-sql.ts +70 -8
  51. package/src/postgres/specification-sql.ts +36 -8
  52. package/test/http/csv-validatorSpec.ts +205 -0
  53. package/test/http/result-streamSpec.ts +151 -0
  54. package/test/postgres/postgresReadSpec.ts +192 -14
  55. package/test/postgres/postgresSpecificationSpec.ts +3 -3
  56. package/test/postgres/specificationConditionsSpec.ts +268 -0
  57. package/documentation/streaming-feeds-full-results-solution.md +0 -230
@@ -0,0 +1,350 @@
1
+ const request = require('supertest');
2
+ const { JinagaServer } = require('./jinaga-server');
3
+ const express = require('express');
4
+
5
+ const host = "db";
6
+ const connectionString = `postgresql://dev:devpw@${host}:5432/integrationtest`;
7
+
8
+ // Test model classes
9
+ class Root {
10
+ static Type = "CSVTest.Root";
11
+ type = Root.Type;
12
+
13
+ constructor(identifier) {
14
+ this.identifier = identifier;
15
+ }
16
+ }
17
+
18
+ class Successor {
19
+ static Type = "CSVTest.Successor";
20
+ type = Successor.Type;
21
+
22
+ constructor(identifier, predecessor) {
23
+ this.identifier = identifier;
24
+ this.pred = predecessor;
25
+ }
26
+ }
27
+
28
+ // Helper to create test app
29
+ async function createTestApp() {
30
+ const { handler, j, close } = JinagaServer.create({
31
+ pgKeystore: connectionString,
32
+ pgStore: connectionString
33
+ });
34
+
35
+ const app = express();
36
+ app.use(express.json());
37
+ app.use(express.text());
38
+ app.use(handler);
39
+
40
+ return { app, j, close };
41
+ }
42
+
43
+ describe('Read Endpoint - CSV Output', () => {
44
+ let app;
45
+ let j;
46
+ let close;
47
+ let root;
48
+ let rootHash;
49
+
50
+ beforeEach(async () => {
51
+ ({ app, j, close } = await createTestApp());
52
+ await j.local();
53
+
54
+ // Create test data
55
+ root = await j.fact(new Root('csv-test-' + Date.now()));
56
+ await j.fact(new Successor(`csv-successor-1`, root));
57
+ await j.fact(new Successor(`csv-successor-2`, root));
58
+ await j.fact(new Successor(`csv-successor-3`, root));
59
+ rootHash = j.hash(root);
60
+ });
61
+
62
+ afterEach(async () => {
63
+ if (close) {
64
+ await close();
65
+ }
66
+ });
67
+
68
+ describe('CSV with Specification Headers', () => {
69
+ it('should use specification labels as CSV headers', async () => {
70
+ const specification = `let root: CSVTest.Root = #${rootHash}
71
+
72
+ (root: CSVTest.Root) {
73
+ s: CSVTest.Successor [
74
+ s->pred: CSVTest.Root = root
75
+ ]
76
+ } => {
77
+ successorId = s.identifier
78
+ successorHash = #s
79
+ successorTime = @s
80
+ }`;
81
+
82
+ const response = await request(app)
83
+ .post('/read')
84
+ .set('Content-Type', 'text/plain')
85
+ .set('Accept', 'text/csv')
86
+ .send(specification);
87
+
88
+ if (response.status !== 200) {
89
+ expect(response.text).toBe('');
90
+ expect(response.status).toBe(200);
91
+ }
92
+ expect(response.type).toBe('text/csv');
93
+
94
+ // Parse CSV
95
+ const lines = response.text.trim().split('\n');
96
+ const headers = lines[0].split(',').map(h => h.trim());
97
+
98
+ // Headers should match specification labels
99
+ expect(headers).toContain('successorId');
100
+ expect(headers).toContain('successorHash');
101
+ expect(headers).toContain('successorTime');
102
+
103
+ // Should have data rows
104
+ expect(lines.length).toBe(4);
105
+
106
+ // Successor id should be the known strings
107
+ expect(lines[1].split(',')[0]).toBe('csv-successor-1');
108
+ expect(lines[2].split(',')[0]).toBe('csv-successor-2');
109
+ expect(lines[3].split(',')[0]).toBe('csv-successor-3');
110
+
111
+ // Successor hash should be the known hashes
112
+ expect(lines[1].split(',')[1]).toBe(j.hash(new Successor('csv-successor-1', root)));
113
+ expect(lines[2].split(',')[1]).toBe(j.hash(new Successor('csv-successor-2', root)));
114
+ expect(lines[3].split(',')[1]).toBe(j.hash(new Successor('csv-successor-3', root)));
115
+
116
+ // Successor time should be recent timestamps
117
+ function expectRecentTimestamp(line) {
118
+ const timestamp = new Date(line.split(',')[2]).getTime();
119
+ expect(timestamp).toBeLessThanOrEqual(Date.now());
120
+ expect(timestamp).toBeGreaterThan(Date.now() - 10000);
121
+ }
122
+ expectRecentTimestamp(lines[1]);
123
+ expectRecentTimestamp(lines[2]);
124
+ expectRecentTimestamp(lines[3]);
125
+ });
126
+
127
+ it('should include headers even with empty result set', async () => {
128
+ const emptyRoot = await j.fact(new Root('empty-csv-test-' + Date.now()));
129
+ const emptyRootHash = j.hash(emptyRoot);
130
+
131
+ const specification = `let root: CSVTest.Root = #${emptyRootHash}
132
+
133
+ (root: CSVTest.Root) {
134
+ s: CSVTest.Successor [
135
+ s->pred: CSVTest.Root = root
136
+ ]
137
+ } => {
138
+ successorName = s.identifier
139
+ successorHash = #s
140
+ }`;
141
+
142
+ const response = await request(app)
143
+ .post('/read')
144
+ .set('Content-Type', 'text/plain')
145
+ .set('Accept', 'text/csv')
146
+ .send(specification);
147
+
148
+ if (response.status !== 200) {
149
+ expect(response.text).toBe('');
150
+ expect(response.status).toBe(200);
151
+ }
152
+ expect(response.type).toBe('text/csv');
153
+
154
+ // Parse CSV
155
+ const lines = response.text.trim().split('\n');
156
+
157
+ // Should have headers even with no data
158
+ expect(lines.length).toBeGreaterThanOrEqual(1);
159
+
160
+ const headers = lines[0].split(',').map(h => h.trim());
161
+ expect(headers).toContain('successorName');
162
+ expect(headers).toContain('successorHash');
163
+ });
164
+
165
+ it('should properly escape CSV values with commas', async () => {
166
+ const specification = `let root: CSVTest.Root = #${rootHash}
167
+
168
+ (root: CSVTest.Root) {
169
+ s: CSVTest.Successor [
170
+ s->pred: CSVTest.Root = root
171
+ ]
172
+ } => {
173
+ identifier = s.identifier
174
+ type = s.type
175
+ }`;
176
+
177
+ const response = await request(app)
178
+ .post('/read')
179
+ .set('Content-Type', 'text/plain')
180
+ .set('Accept', 'text/csv')
181
+ .send(specification);
182
+
183
+ if (response.status !== 200) {
184
+ expect(response.text).toBe('');
185
+ expect(response.status).toBe(200);
186
+ }
187
+ expect(response.type).toBe('text/csv');
188
+
189
+ // Response should be valid CSV
190
+ expect(response.text).toBeTruthy();
191
+ expect(response.text.includes('\n')).toBe(true);
192
+ });
193
+ });
194
+
195
+ describe('CSV Validation', () => {
196
+ it('should reject nested object projections', async () => {
197
+ const specification = `let root: CSVTest.Root = #${rootHash}
198
+
199
+ (root: CSVTest.Root) {
200
+ s: CSVTest.Successor [
201
+ s->pred: CSVTest.Root = root
202
+ ]
203
+ } => {
204
+ nested = {
205
+ grandchild: CSVTest.Grandchild [
206
+ grandchild->parent: CSVTest.Successor = s
207
+ ]
208
+ } => {
209
+ grandchildHash = #grandchild
210
+ }
211
+ }`;
212
+
213
+ const response = await request(app)
214
+ .post('/read')
215
+ .set('Content-Type', 'text/plain')
216
+ .set('Accept', 'text/csv')
217
+ .send(specification);
218
+
219
+ if (response.status !== 400) {
220
+ expect(response.text).toBe('');
221
+ expect(response.status).toBe(400);
222
+ }
223
+ expect(response.text).toContain('not compatible with CSV');
224
+ expect(response.text).toContain(`Unsupported projection type of field 'nested' for CSV export.`);
225
+ });
226
+
227
+ it('should accept flat projections with multiple fields', async () => {
228
+ const specification = `let root: CSVTest.Root = #${rootHash}
229
+
230
+ (root: CSVTest.Root) {
231
+ s: CSVTest.Successor [
232
+ s->pred: CSVTest.Root = root
233
+ ]
234
+ } => {
235
+ id = s.identifier
236
+ type = s.type
237
+ hash = #s
238
+ }`;
239
+
240
+ const response = await request(app)
241
+ .post('/read')
242
+ .set('Content-Type', 'text/plain')
243
+ .set('Accept', 'text/csv')
244
+ .send(specification);
245
+
246
+ if (response.status !== 200) {
247
+ expect(response.text).toBe('');
248
+ expect(response.status).toBe(200);
249
+ }
250
+ expect(response.type).toBe('text/csv');
251
+
252
+ const lines = response.text.trim().split('\n');
253
+ const headers = lines[0].split(',').map(h => h.trim());
254
+
255
+ expect(headers).toContain('id');
256
+ expect(headers).toContain('type');
257
+ expect(headers).toContain('hash');
258
+ });
259
+
260
+ it('should provide helpful error message for invalid projections', async () => {
261
+ const specification = `let root: CSVTest.Root = #${rootHash}
262
+
263
+ (root: CSVTest.Root) {
264
+ s: CSVTest.Successor [
265
+ s->pred: CSVTest.Root = root
266
+ ]
267
+ } => {
268
+ allSuccessors = s
269
+ }`;
270
+
271
+ const response = await request(app)
272
+ .post('/read')
273
+ .set('Content-Type', 'text/plain')
274
+ .set('Accept', 'text/csv')
275
+ .send(specification);
276
+
277
+ if (response.status !== 400) {
278
+ expect(response.text).toBe('');
279
+ expect(response.status).toBe(400);
280
+ }
281
+ expect(response.text).toContain('Hint');
282
+ expect(response.text).toContain('flat projections');
283
+ });
284
+ });
285
+
286
+ describe('CSV with csv-stringify', () => {
287
+ it('should handle special characters properly', async () => {
288
+ // Create successor with special characters in identifier
289
+ const specialRoot = await j.fact(new Root('special-test-' + Date.now()));
290
+ const specialRootHash = j.hash(specialRoot);
291
+ await j.fact(new Successor('test, with "quotes"', specialRoot));
292
+
293
+ const specification = `let root: CSVTest.Root = #${specialRootHash}
294
+
295
+ (root: CSVTest.Root) {
296
+ s: CSVTest.Successor [
297
+ s->pred: CSVTest.Root = root
298
+ ]
299
+ } => {
300
+ name = s.identifier
301
+ }`;
302
+
303
+ const response = await request(app)
304
+ .post('/read')
305
+ .set('Content-Type', 'text/plain')
306
+ .set('Accept', 'text/csv')
307
+ .send(specification);
308
+
309
+ if (response.status !== 200) {
310
+ expect(response.text).toBe('');
311
+ expect(response.status).toBe(200);
312
+ }
313
+ expect(response.type).toBe('text/csv');
314
+
315
+ // Response should properly escape the value
316
+ expect(response.text).toContain('name');
317
+ // csv-stringify should handle the quotes and comma
318
+ expect(response.text).toBeTruthy();
319
+ });
320
+
321
+ it('should handle date values properly', async () => {
322
+ const specification = `let root: CSVTest.Root = #${rootHash}
323
+
324
+ (root: CSVTest.Root) {
325
+ s: CSVTest.Successor [
326
+ s->pred: CSVTest.Root = root
327
+ ]
328
+ } => {
329
+ identifier = s.identifier
330
+ type = s.type
331
+ }`;
332
+
333
+ const response = await request(app)
334
+ .post('/read')
335
+ .set('Content-Type', 'text/plain')
336
+ .set('Accept', 'text/csv')
337
+ .send(specification);
338
+
339
+ if (response.status !== 200) {
340
+ expect(response.text).toBe('');
341
+ expect(response.status).toBe(200);
342
+ }
343
+ expect(response.type).toBe('text/csv');
344
+
345
+ // Should successfully generate CSV
346
+ const lines = response.text.trim().split('\n');
347
+ expect(lines.length).toBeGreaterThan(1);
348
+ });
349
+ });
350
+ });