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.
Files changed (64) 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 +276 -70
  17. package/dist/http/router.js.map +1 -1
  18. package/dist/http/stream.d.ts +3 -0
  19. package/dist/http/stream.js +79 -5
  20. package/dist/http/stream.js.map +1 -1
  21. package/dist/postgres/postgres-store.js +7 -5
  22. package/dist/postgres/postgres-store.js.map +1 -1
  23. package/dist/postgres/purge-sql.js +15 -2
  24. package/dist/postgres/purge-sql.js.map +1 -1
  25. package/dist/postgres/specification-result-sql.d.ts +2 -0
  26. package/dist/postgres/specification-result-sql.js +49 -5
  27. package/dist/postgres/specification-result-sql.js.map +1 -1
  28. package/dist/postgres/specification-sql.js +28 -6
  29. package/dist/postgres/specification-sql.js.map +1 -1
  30. package/docs/analysis/DIAGNOSTIC_LOGGING_README.md +201 -0
  31. package/docs/analysis/security-audit-feeds-hash-handler.md +407 -0
  32. package/docs/design/feeds-persistent-proxy-plan.md +294 -0
  33. package/integration-test/integration-test/jinaga-test/package-lock.json +6 -0
  34. package/integration-test/jinaga-test/Dockerfile +1 -6
  35. package/integration-test/jinaga-test/http-test-helpers.js +77 -0
  36. package/integration-test/jinaga-test/jinaga.test.js +9 -9
  37. package/integration-test/jinaga-test/package-lock.json +329 -32
  38. package/integration-test/jinaga-test/package.json +5 -1
  39. package/integration-test/jinaga-test/read-csv.test.js +330 -0
  40. package/integration-test/jinaga-test/read-endpoint.test.js +442 -0
  41. package/integration-test/jinaga-test/read-performance.test.js +221 -0
  42. package/integration-test/jinaga-test/read-streaming.test.js +199 -0
  43. package/integration-test/jinaga-test/specification-conditions.test.js +147 -0
  44. package/integration-test/jinaga-test/test-helpers.js +12 -0
  45. package/integration-test/jinaga-test/time-projection.test.js +235 -0
  46. package/package.json +3 -2
  47. package/src/authorization/authorization-keystore.ts +4 -4
  48. package/src/http/csv-metadata.ts +34 -0
  49. package/src/http/csv-validator.ts +90 -0
  50. package/src/http/output-formatters.ts +196 -0
  51. package/src/http/result-stream.ts +69 -0
  52. package/src/http/router.ts +344 -62
  53. package/src/http/stream.ts +97 -5
  54. package/src/postgres/postgres-store.ts +13 -6
  55. package/src/postgres/purge-sql.ts +18 -2
  56. package/src/postgres/specification-result-sql.ts +70 -8
  57. package/src/postgres/specification-sql.ts +36 -8
  58. package/test/http/csv-validatorSpec.ts +205 -0
  59. package/test/http/result-streamSpec.ts +151 -0
  60. package/test/postgres/postgresReadSpec.ts +192 -14
  61. package/test/postgres/postgresSpecificationSpec.ts +3 -3
  62. package/test/postgres/specificationConditionsSpec.ts +268 -0
  63. package/test-concurrent-streams.js +219 -0
  64. 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
+ }