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.
- package/dist/authorization/authorization-keystore.js +2 -2
- package/dist/authorization/authorization-keystore.js.map +1 -1
- package/dist/http/csv-metadata.d.ts +24 -0
- package/dist/http/csv-metadata.js +3 -0
- package/dist/http/csv-metadata.js.map +1 -0
- package/dist/http/csv-validator.d.ts +23 -0
- package/dist/http/csv-validator.js +83 -0
- package/dist/http/csv-validator.js.map +1 -0
- package/dist/http/output-formatters.d.ts +28 -0
- package/dist/http/output-formatters.js +197 -0
- package/dist/http/output-formatters.js.map +1 -0
- package/dist/http/result-stream.d.ts +35 -0
- package/dist/http/result-stream.js +78 -0
- package/dist/http/result-stream.js.map +1 -0
- package/dist/http/router.d.ts +7 -1
- package/dist/http/router.js +98 -47
- package/dist/http/router.js.map +1 -1
- package/dist/postgres/postgres-store.js +7 -5
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/purge-sql.js +15 -2
- package/dist/postgres/purge-sql.js.map +1 -1
- package/dist/postgres/specification-result-sql.d.ts +2 -0
- package/dist/postgres/specification-result-sql.js +49 -5
- package/dist/postgres/specification-result-sql.js.map +1 -1
- package/dist/postgres/specification-sql.js +28 -6
- package/dist/postgres/specification-sql.js.map +1 -1
- package/docs/design/feeds-persistent-proxy-plan.md +294 -0
- package/integration-test/integration-test/jinaga-test/package-lock.json +6 -0
- package/integration-test/jinaga-test/Dockerfile +1 -6
- package/integration-test/jinaga-test/http-test-helpers.js +77 -0
- package/integration-test/jinaga-test/jinaga.test.js +9 -9
- package/integration-test/jinaga-test/package-lock.json +329 -32
- package/integration-test/jinaga-test/package.json +5 -1
- package/integration-test/jinaga-test/read-csv.test.js +350 -0
- package/integration-test/jinaga-test/read-endpoint.test.js +442 -0
- package/integration-test/jinaga-test/read-performance.test.js +221 -0
- package/integration-test/jinaga-test/read-streaming.test.js +199 -0
- package/integration-test/jinaga-test/specification-conditions.test.js +147 -0
- package/integration-test/jinaga-test/test-helpers.js +12 -0
- package/integration-test/jinaga-test/time-projection.test.js +235 -0
- package/package.json +3 -2
- package/src/authorization/authorization-keystore.ts +4 -4
- package/src/http/csv-metadata.ts +34 -0
- package/src/http/csv-validator.ts +90 -0
- package/src/http/output-formatters.ts +196 -0
- package/src/http/result-stream.ts +69 -0
- package/src/http/router.ts +117 -34
- package/src/postgres/postgres-store.ts +13 -6
- package/src/postgres/purge-sql.ts +18 -2
- package/src/postgres/specification-result-sql.ts +70 -8
- package/src/postgres/specification-sql.ts +36 -8
- package/test/http/csv-validatorSpec.ts +205 -0
- package/test/http/result-streamSpec.ts +151 -0
- package/test/postgres/postgresReadSpec.ts +192 -14
- package/test/postgres/postgresSpecificationSpec.ts +3 -3
- package/test/postgres/specificationConditionsSpec.ts +268 -0
- 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
|
+
});
|