jinaga-server 3.5.2 → 3.5.3

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 +93 -46
  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 +330 -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 +112 -33
  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,330 @@
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).toBeGreaterThan(1);
105
+ });
106
+
107
+ it('should include headers even with empty result set', async () => {
108
+ const emptyRoot = await j.fact(new Root('empty-csv-test-' + Date.now()));
109
+ const emptyRootHash = j.hash(emptyRoot);
110
+
111
+ const specification = `let root: CSVTest.Root = #${emptyRootHash}
112
+
113
+ (root: CSVTest.Root) {
114
+ s: CSVTest.Successor [
115
+ s->pred: CSVTest.Root = root
116
+ ]
117
+ } => {
118
+ successorName = s.identifier
119
+ successorHash = #s
120
+ }`;
121
+
122
+ const response = await request(app)
123
+ .post('/read')
124
+ .set('Content-Type', 'text/plain')
125
+ .set('Accept', 'text/csv')
126
+ .send(specification);
127
+
128
+ if (response.status !== 200) {
129
+ expect(response.text).toBe('');
130
+ expect(response.status).toBe(200);
131
+ }
132
+ expect(response.type).toBe('text/csv');
133
+
134
+ // Parse CSV
135
+ const lines = response.text.trim().split('\n');
136
+
137
+ // Should have headers even with no data
138
+ expect(lines.length).toBeGreaterThanOrEqual(1);
139
+
140
+ const headers = lines[0].split(',').map(h => h.trim());
141
+ expect(headers).toContain('successorName');
142
+ expect(headers).toContain('successorHash');
143
+ });
144
+
145
+ it('should properly escape CSV values with commas', async () => {
146
+ const specification = `let root: CSVTest.Root = #${rootHash}
147
+
148
+ (root: CSVTest.Root) {
149
+ s: CSVTest.Successor [
150
+ s->pred: CSVTest.Root = root
151
+ ]
152
+ } => {
153
+ identifier = s.identifier
154
+ type = s.type
155
+ }`;
156
+
157
+ const response = await request(app)
158
+ .post('/read')
159
+ .set('Content-Type', 'text/plain')
160
+ .set('Accept', 'text/csv')
161
+ .send(specification);
162
+
163
+ if (response.status !== 200) {
164
+ expect(response.text).toBe('');
165
+ expect(response.status).toBe(200);
166
+ }
167
+ expect(response.type).toBe('text/csv');
168
+
169
+ // Response should be valid CSV
170
+ expect(response.text).toBeTruthy();
171
+ expect(response.text.includes('\n')).toBe(true);
172
+ });
173
+ });
174
+
175
+ describe('CSV Validation', () => {
176
+ it('should reject nested object projections', async () => {
177
+ const specification = `let root: CSVTest.Root = #${rootHash}
178
+
179
+ (root: CSVTest.Root) {
180
+ s: CSVTest.Successor [
181
+ s->pred: CSVTest.Root = root
182
+ ]
183
+ } => {
184
+ nested = {
185
+ grandchild: CSVTest.Grandchild [
186
+ grandchild->parent: CSVTest.Successor = s
187
+ ]
188
+ } => {
189
+ grandchildHash = #grandchild
190
+ }
191
+ }`;
192
+
193
+ const response = await request(app)
194
+ .post('/read')
195
+ .set('Content-Type', 'text/plain')
196
+ .set('Accept', 'text/csv')
197
+ .send(specification);
198
+
199
+ if (response.status !== 400) {
200
+ expect(response.text).toBe('');
201
+ expect(response.status).toBe(400);
202
+ }
203
+ expect(response.text).toContain('not compatible with CSV');
204
+ expect(response.text).toContain(`Unsupported projection type of field 'nested' for CSV export.`);
205
+ });
206
+
207
+ it('should accept flat projections with multiple fields', async () => {
208
+ const specification = `let root: CSVTest.Root = #${rootHash}
209
+
210
+ (root: CSVTest.Root) {
211
+ s: CSVTest.Successor [
212
+ s->pred: CSVTest.Root = root
213
+ ]
214
+ } => {
215
+ id = s.identifier
216
+ type = s.type
217
+ hash = #s
218
+ }`;
219
+
220
+ const response = await request(app)
221
+ .post('/read')
222
+ .set('Content-Type', 'text/plain')
223
+ .set('Accept', 'text/csv')
224
+ .send(specification);
225
+
226
+ if (response.status !== 200) {
227
+ expect(response.text).toBe('');
228
+ expect(response.status).toBe(200);
229
+ }
230
+ expect(response.type).toBe('text/csv');
231
+
232
+ const lines = response.text.trim().split('\n');
233
+ const headers = lines[0].split(',').map(h => h.trim());
234
+
235
+ expect(headers).toContain('id');
236
+ expect(headers).toContain('type');
237
+ expect(headers).toContain('hash');
238
+ });
239
+
240
+ it('should provide helpful error message for invalid projections', async () => {
241
+ const specification = `let root: CSVTest.Root = #${rootHash}
242
+
243
+ (root: CSVTest.Root) {
244
+ s: CSVTest.Successor [
245
+ s->pred: CSVTest.Root = root
246
+ ]
247
+ } => {
248
+ allSuccessors = s
249
+ }`;
250
+
251
+ const response = await request(app)
252
+ .post('/read')
253
+ .set('Content-Type', 'text/plain')
254
+ .set('Accept', 'text/csv')
255
+ .send(specification);
256
+
257
+ if (response.status !== 400) {
258
+ expect(response.text).toBe('');
259
+ expect(response.status).toBe(400);
260
+ }
261
+ expect(response.text).toContain('Hint');
262
+ expect(response.text).toContain('flat projections');
263
+ });
264
+ });
265
+
266
+ describe('CSV with csv-stringify', () => {
267
+ it('should handle special characters properly', async () => {
268
+ // Create successor with special characters in identifier
269
+ const specialRoot = await j.fact(new Root('special-test-' + Date.now()));
270
+ const specialRootHash = j.hash(specialRoot);
271
+ await j.fact(new Successor('test, with "quotes"', specialRoot));
272
+
273
+ const specification = `let root: CSVTest.Root = #${specialRootHash}
274
+
275
+ (root: CSVTest.Root) {
276
+ s: CSVTest.Successor [
277
+ s->pred: CSVTest.Root = root
278
+ ]
279
+ } => {
280
+ name = s.identifier
281
+ }`;
282
+
283
+ const response = await request(app)
284
+ .post('/read')
285
+ .set('Content-Type', 'text/plain')
286
+ .set('Accept', 'text/csv')
287
+ .send(specification);
288
+
289
+ if (response.status !== 200) {
290
+ expect(response.text).toBe('');
291
+ expect(response.status).toBe(200);
292
+ }
293
+ expect(response.type).toBe('text/csv');
294
+
295
+ // Response should properly escape the value
296
+ expect(response.text).toContain('name');
297
+ // csv-stringify should handle the quotes and comma
298
+ expect(response.text).toBeTruthy();
299
+ });
300
+
301
+ it('should handle date values properly', async () => {
302
+ const specification = `let root: CSVTest.Root = #${rootHash}
303
+
304
+ (root: CSVTest.Root) {
305
+ s: CSVTest.Successor [
306
+ s->pred: CSVTest.Root = root
307
+ ]
308
+ } => {
309
+ identifier = s.identifier
310
+ type = s.type
311
+ }`;
312
+
313
+ const response = await request(app)
314
+ .post('/read')
315
+ .set('Content-Type', 'text/plain')
316
+ .set('Accept', 'text/csv')
317
+ .send(specification);
318
+
319
+ if (response.status !== 200) {
320
+ expect(response.text).toBe('');
321
+ expect(response.status).toBe(200);
322
+ }
323
+ expect(response.type).toBe('text/csv');
324
+
325
+ // Should successfully generate CSV
326
+ const lines = response.text.trim().split('\n');
327
+ expect(lines.length).toBeGreaterThan(1);
328
+ });
329
+ });
330
+ });