jinaga-server 3.5.1 → 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.
- 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 +276 -70
- package/dist/http/router.js.map +1 -1
- package/dist/http/stream.d.ts +3 -0
- package/dist/http/stream.js +79 -5
- package/dist/http/stream.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/analysis/DIAGNOSTIC_LOGGING_README.md +201 -0
- package/docs/analysis/security-audit-feeds-hash-handler.md +407 -0
- 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 +330 -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 +344 -62
- package/src/http/stream.ts +97 -5
- 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/test-concurrent-streams.js +219 -0
- package/documentation/streaming-feeds-full-results-solution.md +0 -230
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Specification } from 'jinaga';
|
|
2
|
+
import { CsvMetadata } from './csv-metadata';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates a specification for CSV compatibility.
|
|
6
|
+
* Returns metadata including headers and validation results.
|
|
7
|
+
*
|
|
8
|
+
* CSV requires flat projections with single-valued components:
|
|
9
|
+
* - ✅ Fields (scalar values from matched facts)
|
|
10
|
+
* - ✅ Hashes
|
|
11
|
+
* - ✅ Timestamps
|
|
12
|
+
* - ❌ Arrays (nested specifications)
|
|
13
|
+
* - ❌ Nested objects (composite projections)
|
|
14
|
+
*/
|
|
15
|
+
export function validateSpecificationForCsv(specification: Specification): CsvMetadata {
|
|
16
|
+
const headers: string[] = [];
|
|
17
|
+
const errors: string[] = [];
|
|
18
|
+
|
|
19
|
+
// Ensure projection is composite
|
|
20
|
+
if (specification.projection.type !== 'composite') {
|
|
21
|
+
return {
|
|
22
|
+
headers: [],
|
|
23
|
+
isValid: false,
|
|
24
|
+
errors: ['Specification projection must be composite for CSV export']
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Ensure that the composite has components.
|
|
29
|
+
// An empty composite represents a missing projection.
|
|
30
|
+
if (specification.projection.components.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
headers: [],
|
|
33
|
+
isValid: false,
|
|
34
|
+
errors: ['Specification must have a projection for CSV export']
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const component of specification.projection.components) {
|
|
39
|
+
if (component.type === 'field' || component.type === 'hash' || component.type === 'time') {
|
|
40
|
+
headers.push(component.name);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
errors.push(`Unsupported projection type of field '${component.name}' for CSV export. Only flat field projections are allowed.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
headers,
|
|
49
|
+
isValid: errors.length === 0,
|
|
50
|
+
errors
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract value from result object using projection label.
|
|
56
|
+
* Since CSV only supports flat projections, labels are simple property names.
|
|
57
|
+
*/
|
|
58
|
+
export function extractValueByLabel(result: any, label: string): any {
|
|
59
|
+
if (result == null) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Direct property access
|
|
64
|
+
const value = result[label];
|
|
65
|
+
|
|
66
|
+
// Return null instead of undefined
|
|
67
|
+
return value ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format a value for CSV output
|
|
72
|
+
*/
|
|
73
|
+
export function formatValueForCsv(value: any): string {
|
|
74
|
+
if (value === null || value === undefined) {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Handle dates
|
|
79
|
+
if (value instanceof Date) {
|
|
80
|
+
return value.toISOString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle booleans
|
|
84
|
+
if (typeof value === 'boolean') {
|
|
85
|
+
return value ? 'true' : 'false';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Default: convert to string
|
|
89
|
+
return String(value);
|
|
90
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { stringify } from 'csv-stringify';
|
|
2
|
+
import { Response } from "express";
|
|
3
|
+
import { CsvMetadata } from "./csv-metadata";
|
|
4
|
+
import { extractValueByLabel } from "./csv-validator";
|
|
5
|
+
import { ResultStream } from "./result-stream";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Output read results with streaming support.
|
|
9
|
+
* Handles content negotiation and streams data when possible.
|
|
10
|
+
*/
|
|
11
|
+
export async function outputReadResultsStreaming(
|
|
12
|
+
result: ResultStream<any>,
|
|
13
|
+
res: Response,
|
|
14
|
+
acceptType: string,
|
|
15
|
+
csvMetadata?: CsvMetadata
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
switch (acceptType) {
|
|
18
|
+
case "application/x-ndjson":
|
|
19
|
+
// NDJSON format - stream one JSON object per line
|
|
20
|
+
await streamAsNDJSON(result, res);
|
|
21
|
+
break;
|
|
22
|
+
case "text/csv":
|
|
23
|
+
// CSV format - stream as CSV using csv-stringify
|
|
24
|
+
if (csvMetadata) {
|
|
25
|
+
await streamAsCSVWithStringify(result, res, csvMetadata);
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error("CSV metadata is required for CSV output format");
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case "application/json":
|
|
31
|
+
// Compact JSON
|
|
32
|
+
await collectAndSendCompactJSON(result, res);
|
|
33
|
+
break;
|
|
34
|
+
case "text/plain":
|
|
35
|
+
// Pretty-printed JSON
|
|
36
|
+
await collectAndSendPrettyJSON(result, res);
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
// Default to pretty-printed JSON for unrecognized types
|
|
40
|
+
await collectAndSendPrettyJSON(result, res);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Stream results as NDJSON (newline-delimited JSON).
|
|
47
|
+
* Each result is written as a separate line.
|
|
48
|
+
*/
|
|
49
|
+
export async function streamAsNDJSON(stream: ResultStream<any>, res: Response): Promise<void> {
|
|
50
|
+
res.type("application/x-ndjson");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
let item;
|
|
54
|
+
while ((item = await stream.next()) !== null) {
|
|
55
|
+
res.write(JSON.stringify(item) + '\n');
|
|
56
|
+
}
|
|
57
|
+
res.end();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Send error as NDJSON line
|
|
60
|
+
const errorFrame = {
|
|
61
|
+
error: true,
|
|
62
|
+
message: error instanceof Error ? error.message : String(error)
|
|
63
|
+
};
|
|
64
|
+
res.write(JSON.stringify(errorFrame) + '\n');
|
|
65
|
+
res.end();
|
|
66
|
+
} finally {
|
|
67
|
+
await stream.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Collect all results from stream and send as compact JSON.
|
|
73
|
+
* Used for application/json format.
|
|
74
|
+
*/
|
|
75
|
+
export async function collectAndSendCompactJSON(
|
|
76
|
+
stream: ResultStream<any>,
|
|
77
|
+
res: Response
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
const results: any[] = [];
|
|
81
|
+
let item;
|
|
82
|
+
while ((item = await stream.next()) !== null) {
|
|
83
|
+
results.push(item);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
res.type("application/json");
|
|
87
|
+
res.send(JSON.stringify(results));
|
|
88
|
+
} finally {
|
|
89
|
+
await stream.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Collect all results from stream and send as pretty-printed JSON.
|
|
95
|
+
* Used for text/plain format and as default for unrecognized Accept types.
|
|
96
|
+
*/
|
|
97
|
+
export async function collectAndSendPrettyJSON(
|
|
98
|
+
stream: ResultStream<any>,
|
|
99
|
+
res: Response
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
try {
|
|
102
|
+
const results: any[] = [];
|
|
103
|
+
let item;
|
|
104
|
+
while ((item = await stream.next()) !== null) {
|
|
105
|
+
results.push(item);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
res.type("text/plain");
|
|
109
|
+
res.send(JSON.stringify(results, null, 2));
|
|
110
|
+
} finally {
|
|
111
|
+
await stream.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Stream results as CSV using csv-stringify library.
|
|
117
|
+
* Headers are provided from the specification metadata.
|
|
118
|
+
*/
|
|
119
|
+
export async function streamAsCSVWithStringify(
|
|
120
|
+
stream: ResultStream<any>,
|
|
121
|
+
res: Response,
|
|
122
|
+
csvMetadata: CsvMetadata
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
res.type("text/csv");
|
|
125
|
+
|
|
126
|
+
// Helper to await 'finish' or 'error' events on a stream
|
|
127
|
+
function finishedAsync(stream: NodeJS.EventEmitter): Promise<void> {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
stream.once('finish', resolve);
|
|
130
|
+
stream.once('error', reject);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let stringifier: any;
|
|
135
|
+
try {
|
|
136
|
+
// Create csv-stringify stringifier with headers from specification
|
|
137
|
+
stringifier = stringify({
|
|
138
|
+
header: true,
|
|
139
|
+
columns: csvMetadata.headers,
|
|
140
|
+
cast: {
|
|
141
|
+
// Custom casting for special types
|
|
142
|
+
boolean: (value) => value ? 'true' : 'false',
|
|
143
|
+
date: (value) => {
|
|
144
|
+
if (value instanceof Date) {
|
|
145
|
+
return value.toISOString();
|
|
146
|
+
}
|
|
147
|
+
return String(value);
|
|
148
|
+
},
|
|
149
|
+
object: (value) => {
|
|
150
|
+
// Handle nested objects
|
|
151
|
+
if (value && typeof value === 'object') {
|
|
152
|
+
// Fact reference - just use hash
|
|
153
|
+
if (value.type && value.hash) {
|
|
154
|
+
return value.hash;
|
|
155
|
+
}
|
|
156
|
+
// Other objects - stringify
|
|
157
|
+
return JSON.stringify(value);
|
|
158
|
+
}
|
|
159
|
+
return String(value);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Pipe stringifier to response
|
|
165
|
+
stringifier.pipe(res);
|
|
166
|
+
|
|
167
|
+
// Stream data through stringifier
|
|
168
|
+
let item: any = null;
|
|
169
|
+
while ((item = await stream.next()) !== null) {
|
|
170
|
+
// Extract values in header order
|
|
171
|
+
const row: any = {};
|
|
172
|
+
for (const header of csvMetadata.headers) {
|
|
173
|
+
const value = extractValueByLabel(item, header);
|
|
174
|
+
row[header] = value !== null && value !== undefined ? value : '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Write row to stringifier
|
|
178
|
+
stringifier.write(row);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Signal end of data
|
|
182
|
+
stringifier.end();
|
|
183
|
+
|
|
184
|
+
// Await finish or error
|
|
185
|
+
await finishedAsync(stringifier);
|
|
186
|
+
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error('Error in CSV streaming:', error);
|
|
189
|
+
if (!res.headersSent) {
|
|
190
|
+
res.status(500).send('Error generating CSV');
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
} finally {
|
|
194
|
+
await stream.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultStream interface for streaming query results incrementally.
|
|
3
|
+
* This abstraction allows for memory-efficient processing of large result sets.
|
|
4
|
+
*/
|
|
5
|
+
export interface ResultStream<T> {
|
|
6
|
+
/**
|
|
7
|
+
* Get the next item in the stream.
|
|
8
|
+
* @returns Promise resolving to the next item, or null if stream is exhausted
|
|
9
|
+
*/
|
|
10
|
+
next(): Promise<T | null>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Close the stream and release resources.
|
|
14
|
+
*/
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Implements ResultStream for AsyncIterable sources.
|
|
20
|
+
* Useful for streaming data from database cursors or other async sources.
|
|
21
|
+
*/
|
|
22
|
+
export class AsyncIterableResultStream<T> implements ResultStream<T> {
|
|
23
|
+
private iterator: AsyncIterator<T>;
|
|
24
|
+
private closed: boolean = false;
|
|
25
|
+
|
|
26
|
+
constructor(iterable: AsyncIterable<T>) {
|
|
27
|
+
this.iterator = iterable[Symbol.asyncIterator]();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async next(): Promise<T | null> {
|
|
31
|
+
if (this.closed) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = await this.iterator.next();
|
|
36
|
+
if (result.done) {
|
|
37
|
+
await this.close();
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result.value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async close(): Promise<void> {
|
|
45
|
+
if (!this.closed) {
|
|
46
|
+
this.closed = true;
|
|
47
|
+
if (this.iterator.return) {
|
|
48
|
+
await this.iterator.return();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Converts an array to a ResultStream.
|
|
56
|
+
* Useful for backward compatibility and testing.
|
|
57
|
+
*/
|
|
58
|
+
export async function* arrayAsyncIterable<T>(array: T[]): AsyncIterable<T> {
|
|
59
|
+
for (const item of array) {
|
|
60
|
+
yield item;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Helper function to convert an array to a ResultStream.
|
|
66
|
+
*/
|
|
67
|
+
export function arrayToResultStream<T>(array: T[]): ResultStream<T> {
|
|
68
|
+
return new AsyncIterableResultStream(arrayAsyncIterable(array));
|
|
69
|
+
}
|