turbine-orm 0.60.1 → 0.62.0
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/README.md +71 -27
- package/dist/cjs/cli/config.d.ts +40 -0
- package/dist/cjs/cli/config.js +74 -2
- package/dist/cjs/cli/index.d.ts +85 -1
- package/dist/cjs/cli/index.js +374 -24
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +448 -29
- package/dist/cjs/cli/pii-tags.d.ts +64 -9
- package/dist/cjs/cli/pii-tags.js +218 -39
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +23 -0
- package/dist/cjs/cli/studio.js +126 -53
- package/dist/cjs/cli/ui.d.ts +15 -1
- package/dist/cjs/cli/ui.js +19 -5
- package/dist/cjs/client.js +248 -11
- package/dist/cjs/errors.d.ts +38 -1
- package/dist/cjs/errors.js +235 -24
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/pipeline-submittable.js +26 -3
- package/dist/cjs/pipeline.js +15 -2
- package/dist/cjs/powql.d.ts +12 -0
- package/dist/cjs/powql.js +46 -21
- package/dist/cjs/prisma-compat.d.ts +15 -5
- package/dist/cjs/prisma-compat.js +273 -78
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +24 -10
- package/dist/cjs/query/batched-loader.d.ts +9 -4
- package/dist/cjs/query/batched-loader.js +4 -1
- package/dist/cjs/query/builder.d.ts +47 -0
- package/dist/cjs/query/builder.js +149 -21
- package/dist/cjs/query/index.d.ts +3 -1
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/option-surface.d.ts +11 -0
- package/dist/cjs/query/option-surface.js +13 -0
- package/dist/cjs/query/relations.d.ts +8 -0
- package/dist/cjs/query/relations.js +21 -1
- package/dist/cjs/query/types.d.ts +152 -18
- package/dist/cjs/query/types.js +212 -1
- package/dist/cjs/query/where.d.ts +3 -3
- package/dist/cjs/query/where.js +8 -2
- package/dist/cjs/query/writes.js +10 -9
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.js +73 -2
- package/dist/cli/index.d.ts +85 -1
- package/dist/cli/index.js +373 -27
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +448 -29
- package/dist/cli/pii-tags.d.ts +64 -9
- package/dist/cli/pii-tags.js +217 -39
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +23 -0
- package/dist/cli/studio.js +125 -53
- package/dist/cli/ui.d.ts +15 -1
- package/dist/cli/ui.js +18 -4
- package/dist/client.js +250 -13
- package/dist/errors.d.ts +38 -1
- package/dist/errors.js +234 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/dist/pipeline-submittable.js +26 -3
- package/dist/pipeline.js +15 -2
- package/dist/powql.d.ts +12 -0
- package/dist/powql.js +46 -21
- package/dist/prisma-compat.d.ts +15 -5
- package/dist/prisma-compat.js +274 -79
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +24 -10
- package/dist/query/batched-loader.d.ts +9 -4
- package/dist/query/batched-loader.js +4 -1
- package/dist/query/builder.d.ts +47 -0
- package/dist/query/builder.js +148 -21
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -0
- package/dist/query/option-surface.d.ts +11 -0
- package/dist/query/option-surface.js +13 -0
- package/dist/query/relations.d.ts +8 -0
- package/dist/query/relations.js +21 -1
- package/dist/query/types.d.ts +152 -18
- package/dist/query/types.js +207 -2
- package/dist/query/where.d.ts +3 -3
- package/dist/query/where.js +8 -2
- package/dist/query/writes.js +10 -9
- package/package.json +13 -3
|
@@ -9,33 +9,88 @@
|
|
|
9
9
|
* Studio and the MCP server introspect a live database, so on their own they
|
|
10
10
|
* see NO tags at all and their redaction is inert. This module closes that gap
|
|
11
11
|
* by reading the tags out of the generated `metadata.ts` that `turbine
|
|
12
|
-
* generate` writes, and handing back a table
|
|
12
|
+
* generate` writes, and handing back a table -> column-name map the caller
|
|
13
13
|
* layers onto its introspected metadata.
|
|
14
14
|
*
|
|
15
|
-
* The generated file is TypeScript in the user's project, so
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* The generated file is TypeScript in the user's project, so a compiled CLI
|
|
16
|
+
* cannot import it (there is no TS loader in the published `dist`, and running
|
|
17
|
+
* user code to learn which columns to hide would be a worse trade than reading
|
|
18
|
+
* it). It is read as TEXT and scanned STRUCTURALLY. Nothing is executed.
|
|
19
|
+
*
|
|
20
|
+
* WHY STRUCTURALLY, and not with line-anchored regexes (which is what this did
|
|
21
|
+
* until it was found to fail open): the old scan required exactly 8 leading
|
|
22
|
+
* spaces, single-quoted keys, and the whole column object on ONE line. Real
|
|
23
|
+
* emitted column lines run past 120 characters, which is this repo's own Biome
|
|
24
|
+
* `lineWidth`, so ANY formatter run over a user's generated directory rewraps
|
|
25
|
+
* them and every tag silently disappears. Redaction then reported success with
|
|
26
|
+
* an empty column list, indistinguishable from "this table has no PII". The
|
|
27
|
+
* scanner below is whitespace-, quote-, and line-break-insensitive, and when it
|
|
28
|
+
* cannot find the structure at all it says so (`PiiScan.ok === false`) instead
|
|
29
|
+
* of returning an empty map that reads like a clean bill of health. Callers are
|
|
30
|
+
* expected to FAIL CLOSED on `ok === false`.
|
|
31
|
+
*
|
|
32
|
+
* "Cannot find the structure" includes a file that STOPS PART-WAY. A tag is
|
|
33
|
+
* recorded only when its column object CLOSES, so a metadata.ts cut short by an
|
|
34
|
+
* interrupted `turbine generate`, a full disk, or a merge conflict yields fewer
|
|
35
|
+
* tags than the file declares, and every scan-level signal (a `tables:` map, a
|
|
36
|
+
* column object) is already present by then. The scan therefore also requires
|
|
37
|
+
* that the tokenizer reached the end of the file cleanly and that every `{`/`[`
|
|
38
|
+
* it opened was closed; a leftover frame means the tail was lost, and the tags
|
|
39
|
+
* from the lost tail with it.
|
|
20
40
|
*/
|
|
21
|
-
/** Table name
|
|
41
|
+
/** Table name -> the snake_case column names tagged `pii: true`. */
|
|
22
42
|
export type PiiTagMap = Record<string, string[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Outcome of scanning one generated-metadata file.
|
|
45
|
+
*
|
|
46
|
+
* `ok: false` means the scan could not prove anything about this file: it did
|
|
47
|
+
* not look like `generateMetadata` output (no `tables:` map, or not a single
|
|
48
|
+
* parsable column object), or it did not parse to completion (it ends inside a
|
|
49
|
+
* string, a comment, or an unclosed object). An empty `tags` with `ok: true` is
|
|
50
|
+
* a real "no tagged columns"; an empty `tags` with `ok: false` is "we do not
|
|
51
|
+
* know", and the two must never be treated the same way. Nor is a NON-empty
|
|
52
|
+
* `tags` with `ok: false` usable: a truncated file's surviving tags are a
|
|
53
|
+
* prefix of the real set, so trusting them still under-redacts.
|
|
54
|
+
*/
|
|
55
|
+
export interface PiiScan {
|
|
56
|
+
tags: PiiTagMap;
|
|
57
|
+
ok: boolean;
|
|
58
|
+
/** Human-readable cause, present only when `ok` is false. */
|
|
59
|
+
reason?: string;
|
|
60
|
+
/** Table entries seen under `tables:`. */
|
|
61
|
+
tablesSeen: number;
|
|
62
|
+
/** Column objects seen under any table's `columns:` list, tagged or not. */
|
|
63
|
+
columnsSeen: number;
|
|
64
|
+
}
|
|
23
65
|
export interface PiiTagSource {
|
|
24
66
|
/** Absolute path that was read. */
|
|
25
67
|
path: string;
|
|
26
68
|
tags: PiiTagMap;
|
|
27
69
|
/** Total tagged columns, across tables. */
|
|
28
70
|
count: number;
|
|
71
|
+
/** Full scan outcome. Callers that can fail closed should read `scan.ok`. */
|
|
72
|
+
scan: PiiScan;
|
|
29
73
|
}
|
|
30
74
|
/**
|
|
31
|
-
* Scan generated-metadata source text for PII-tagged columns
|
|
75
|
+
* Scan generated-metadata source text for PII-tagged columns, reporting whether
|
|
76
|
+
* the scan actually understood the file.
|
|
32
77
|
*
|
|
33
|
-
* Exported for testing
|
|
78
|
+
* Exported for testing and for callers that need to fail closed; the plain-map
|
|
79
|
+
* convenience wrapper is {@link parsePiiTags}.
|
|
80
|
+
*/
|
|
81
|
+
export declare function scanPiiTags(source: string): PiiScan;
|
|
82
|
+
/**
|
|
83
|
+
* Tag map only. Kept for callers that already treat "no tags" as safe; anything
|
|
84
|
+
* that redacts should prefer {@link scanPiiTags} so it can see a failed scan.
|
|
34
85
|
*/
|
|
35
86
|
export declare function parsePiiTags(source: string): PiiTagMap;
|
|
36
87
|
/**
|
|
37
88
|
* Read PII tags from the generated metadata in `outDir`, or return `null` when
|
|
38
89
|
* there is no readable generated metadata there. Never throws.
|
|
90
|
+
*
|
|
91
|
+
* A returned source with `scan.ok === false` means a file WAS found and could
|
|
92
|
+
* not be understood: that is strictly worse than no file at all, because the
|
|
93
|
+
* user believes their tags are in force. Callers must not treat it as "no PII".
|
|
39
94
|
*/
|
|
40
95
|
export declare function loadPiiTags(outDir: string): PiiTagSource | null;
|
|
41
96
|
/**
|
package/dist/cjs/cli/pii-tags.js
CHANGED
|
@@ -10,68 +10,247 @@
|
|
|
10
10
|
* Studio and the MCP server introspect a live database, so on their own they
|
|
11
11
|
* see NO tags at all and their redaction is inert. This module closes that gap
|
|
12
12
|
* by reading the tags out of the generated `metadata.ts` that `turbine
|
|
13
|
-
* generate` writes, and handing back a table
|
|
13
|
+
* generate` writes, and handing back a table -> column-name map the caller
|
|
14
14
|
* layers onto its introspected metadata.
|
|
15
15
|
*
|
|
16
|
-
* The generated file is TypeScript in the user's project, so
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* The generated file is TypeScript in the user's project, so a compiled CLI
|
|
17
|
+
* cannot import it (there is no TS loader in the published `dist`, and running
|
|
18
|
+
* user code to learn which columns to hide would be a worse trade than reading
|
|
19
|
+
* it). It is read as TEXT and scanned STRUCTURALLY. Nothing is executed.
|
|
20
|
+
*
|
|
21
|
+
* WHY STRUCTURALLY, and not with line-anchored regexes (which is what this did
|
|
22
|
+
* until it was found to fail open): the old scan required exactly 8 leading
|
|
23
|
+
* spaces, single-quoted keys, and the whole column object on ONE line. Real
|
|
24
|
+
* emitted column lines run past 120 characters, which is this repo's own Biome
|
|
25
|
+
* `lineWidth`, so ANY formatter run over a user's generated directory rewraps
|
|
26
|
+
* them and every tag silently disappears. Redaction then reported success with
|
|
27
|
+
* an empty column list, indistinguishable from "this table has no PII". The
|
|
28
|
+
* scanner below is whitespace-, quote-, and line-break-insensitive, and when it
|
|
29
|
+
* cannot find the structure at all it says so (`PiiScan.ok === false`) instead
|
|
30
|
+
* of returning an empty map that reads like a clean bill of health. Callers are
|
|
31
|
+
* expected to FAIL CLOSED on `ok === false`.
|
|
32
|
+
*
|
|
33
|
+
* "Cannot find the structure" includes a file that STOPS PART-WAY. A tag is
|
|
34
|
+
* recorded only when its column object CLOSES, so a metadata.ts cut short by an
|
|
35
|
+
* interrupted `turbine generate`, a full disk, or a merge conflict yields fewer
|
|
36
|
+
* tags than the file declares, and every scan-level signal (a `tables:` map, a
|
|
37
|
+
* column object) is already present by then. The scan therefore also requires
|
|
38
|
+
* that the tokenizer reached the end of the file cleanly and that every `{`/`[`
|
|
39
|
+
* it opened was closed; a leftover frame means the tail was lost, and the tags
|
|
40
|
+
* from the lost tail with it.
|
|
21
41
|
*/
|
|
22
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.scanPiiTags = scanPiiTags;
|
|
23
44
|
exports.parsePiiTags = parsePiiTags;
|
|
24
45
|
exports.loadPiiTags = loadPiiTags;
|
|
25
46
|
exports.applyPiiTags = applyPiiTags;
|
|
26
47
|
const node_fs_1 = require("node:fs");
|
|
27
48
|
const node_path_1 = require("node:path");
|
|
28
|
-
|
|
29
|
-
const TABLE_HEAD = /^ {4}(?:'([^']+)'|([A-Za-z_$][\w$]*)): \{$/;
|
|
30
|
-
/** A serialized column object; `pii: true` is emitted only when tagged. */
|
|
31
|
-
const COLUMN_LINE = /^ {8}\{ name: '([^']+)'.*\bpii: true\b/;
|
|
32
|
-
/** ` columns: [` opens the column list; ` ],` closes it. */
|
|
33
|
-
const COLUMNS_OPEN = /^ {6}columns: \[$/;
|
|
34
|
-
const COLUMNS_CLOSE = /^ {6}\],$/;
|
|
49
|
+
const WORD_CHAR = /[A-Za-z0-9_$.]/;
|
|
35
50
|
/**
|
|
36
|
-
*
|
|
51
|
+
* Tokenize generated-metadata source far enough to walk its object structure:
|
|
52
|
+
* strings (all three quote styles, backslash escapes honoured), bare words, and
|
|
53
|
+
* single-character punctuation. Comments are skipped.
|
|
54
|
+
*
|
|
55
|
+
* Strings are a distinct token kind on purpose: a column's `definition` field
|
|
56
|
+
* carries raw index DDL, which contains braces and parentheses. Treating that
|
|
57
|
+
* text as structure would desynchronize the brace depth and silently drop every
|
|
58
|
+
* later table.
|
|
37
59
|
*
|
|
38
|
-
*
|
|
60
|
+
* A string or block comment that never closes is REPORTED rather than accepted:
|
|
61
|
+
* that is a half-written file, and the tokens before the cut are a partial view
|
|
62
|
+
* of the tags in it.
|
|
39
63
|
*/
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
let
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
function tokenize(source) {
|
|
65
|
+
const tokens = [];
|
|
66
|
+
let truncated = null;
|
|
67
|
+
let i = 0;
|
|
68
|
+
const n = source.length;
|
|
69
|
+
while (i < n) {
|
|
70
|
+
const ch = source[i];
|
|
71
|
+
if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n') {
|
|
72
|
+
i++;
|
|
49
73
|
continue;
|
|
50
74
|
}
|
|
51
|
-
if (
|
|
75
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
76
|
+
const nl = source.indexOf('\n', i);
|
|
77
|
+
i = nl === -1 ? n : nl + 1;
|
|
52
78
|
continue;
|
|
53
|
-
|
|
54
|
-
|
|
79
|
+
}
|
|
80
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
81
|
+
const end = source.indexOf('*/', i + 2);
|
|
82
|
+
if (end === -1)
|
|
83
|
+
truncated = 'the file ends inside an unterminated block comment';
|
|
84
|
+
i = end === -1 ? n : end + 2;
|
|
55
85
|
continue;
|
|
56
86
|
}
|
|
57
|
-
if (
|
|
58
|
-
|
|
87
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
88
|
+
const quote = ch;
|
|
89
|
+
let out = '';
|
|
90
|
+
let closed = false;
|
|
91
|
+
i++;
|
|
92
|
+
while (i < n) {
|
|
93
|
+
const c = source[i];
|
|
94
|
+
if (c === '\\') {
|
|
95
|
+
// Only the escapes `escSQ` and JSON.stringify can emit matter here;
|
|
96
|
+
// anything else is passed through as its literal second character.
|
|
97
|
+
const next = source[i + 1];
|
|
98
|
+
if (next === undefined)
|
|
99
|
+
break;
|
|
100
|
+
out += next === 'n' ? '\n' : next === 't' ? '\t' : next;
|
|
101
|
+
i += 2;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (c === quote) {
|
|
105
|
+
i++;
|
|
106
|
+
closed = true;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
out += c;
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
if (!closed)
|
|
113
|
+
truncated = 'the file ends inside an unterminated string literal';
|
|
114
|
+
tokens.push({ kind: 'string', value: out });
|
|
59
115
|
continue;
|
|
60
116
|
}
|
|
61
|
-
if (
|
|
117
|
+
if (WORD_CHAR.test(ch)) {
|
|
118
|
+
let out = '';
|
|
119
|
+
while (i < n && WORD_CHAR.test(source[i])) {
|
|
120
|
+
out += source[i];
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
tokens.push({ kind: 'word', value: out });
|
|
62
124
|
continue;
|
|
63
|
-
const col = COLUMN_LINE.exec(line);
|
|
64
|
-
if (col?.[1]) {
|
|
65
|
-
const list = tags[table] ?? [];
|
|
66
|
-
list.push(col[1]);
|
|
67
|
-
tags[table] = list;
|
|
68
125
|
}
|
|
126
|
+
tokens.push({ kind: 'punct', value: ch });
|
|
127
|
+
i++;
|
|
69
128
|
}
|
|
70
|
-
return
|
|
129
|
+
return { tokens, truncated };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* True when `stack`'s innermost frame is a column object, i.e. the path ends
|
|
133
|
+
* `... tables -> <tableName> -> columns -> <array element>`.
|
|
134
|
+
*
|
|
135
|
+
* Checking the TAIL rather than an absolute depth is what makes this survive
|
|
136
|
+
* the wrapper the generator puts around the map (`export const SCHEMA: ... = {`)
|
|
137
|
+
* and any future nesting change above `tables`. It also keeps unrelated
|
|
138
|
+
* `columns:` lists out: an index entry's `columns` sits under
|
|
139
|
+
* `... -> indexes -> <element> -> columns`, whose great-grandparent is
|
|
140
|
+
* `indexes`, not `tables`.
|
|
141
|
+
*/
|
|
142
|
+
function isColumnFrame(stack) {
|
|
143
|
+
const len = stack.length;
|
|
144
|
+
if (len < 4)
|
|
145
|
+
return false;
|
|
146
|
+
return (stack[len - 1].key === null && stack[len - 2].key === 'columns' && stack[len - 4].key === 'tables'
|
|
147
|
+
// stack[len - 3] is the table name; any name is valid, so it is not checked.
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Scan generated-metadata source text for PII-tagged columns, reporting whether
|
|
152
|
+
* the scan actually understood the file.
|
|
153
|
+
*
|
|
154
|
+
* Exported for testing and for callers that need to fail closed; the plain-map
|
|
155
|
+
* convenience wrapper is {@link parsePiiTags}.
|
|
156
|
+
*/
|
|
157
|
+
function scanPiiTags(source) {
|
|
158
|
+
const tags = {};
|
|
159
|
+
const stack = [];
|
|
160
|
+
const tableNames = new Set();
|
|
161
|
+
let columnsSeen = 0;
|
|
162
|
+
let sawTablesMap = false;
|
|
163
|
+
let unbalancedClose = false;
|
|
164
|
+
let pendingKey = null;
|
|
165
|
+
const { tokens, truncated } = tokenize(source);
|
|
166
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
167
|
+
const token = tokens[i];
|
|
168
|
+
if (token.kind === 'punct' && (token.value === '{' || token.value === '[')) {
|
|
169
|
+
const frame = { key: pendingKey };
|
|
170
|
+
pendingKey = null;
|
|
171
|
+
stack.push(frame);
|
|
172
|
+
if (frame.key === 'tables')
|
|
173
|
+
sawTablesMap = true;
|
|
174
|
+
if (token.value === '{' && isColumnFrame(stack)) {
|
|
175
|
+
frame.column = {};
|
|
176
|
+
columnsSeen++;
|
|
177
|
+
const tableName = stack[stack.length - 3].key;
|
|
178
|
+
if (tableName !== null)
|
|
179
|
+
tableNames.add(tableName);
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (token.kind === 'punct' && (token.value === '}' || token.value === ']')) {
|
|
184
|
+
const frame = stack.pop();
|
|
185
|
+
// More closers than openers: the walk is no longer tracking real depth,
|
|
186
|
+
// so `isColumnFrame` can no longer be trusted for the rest of the file.
|
|
187
|
+
if (frame === undefined)
|
|
188
|
+
unbalancedClose = true;
|
|
189
|
+
pendingKey = null;
|
|
190
|
+
const column = frame?.column;
|
|
191
|
+
if (column?.pii === true && column.name !== undefined) {
|
|
192
|
+
// stack was already popped, so the table name is now the last-but-one.
|
|
193
|
+
const tableName = stack[stack.length - 2]?.key;
|
|
194
|
+
if (tableName) {
|
|
195
|
+
const list = tags[tableName] ?? [];
|
|
196
|
+
list.push(column.name);
|
|
197
|
+
tags[tableName] = list;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// `<key>:` opens a member. Two things happen: the key becomes the label for
|
|
203
|
+
// whatever container may follow, and, inside a column object, a scalar value
|
|
204
|
+
// on the same member is captured directly.
|
|
205
|
+
const next = tokens[i + 1];
|
|
206
|
+
if ((token.kind === 'string' || token.kind === 'word') && next?.kind === 'punct' && next.value === ':') {
|
|
207
|
+
pendingKey = token.value;
|
|
208
|
+
const column = stack[stack.length - 1]?.column;
|
|
209
|
+
const value = tokens[i + 2];
|
|
210
|
+
if (column && value && !(value.kind === 'punct' && (value.value === '{' || value.value === '['))) {
|
|
211
|
+
if (token.value === 'name' && value.kind === 'string')
|
|
212
|
+
column.name = value.value;
|
|
213
|
+
else if (token.value === 'pii' && value.kind === 'word' && value.value === 'true')
|
|
214
|
+
column.pii = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const tablesSeen = tableNames.size;
|
|
219
|
+
let reason;
|
|
220
|
+
// Truncation is checked FIRST and independently of what was found. A file cut
|
|
221
|
+
// short mid-write still contains a `tables:` map and parsable columns, so the
|
|
222
|
+
// "did this look like generated metadata" questions all answer yes while the
|
|
223
|
+
// tags after the cut are simply missing. That is the shape this scan used to
|
|
224
|
+
// report as `ok: true` with a short tag list.
|
|
225
|
+
if (truncated)
|
|
226
|
+
reason = `${truncated}; the generated metadata file looks truncated`;
|
|
227
|
+
else if (stack.length > 0) {
|
|
228
|
+
reason =
|
|
229
|
+
`the file ends with ${stack.length} unclosed object/array level(s); the generated metadata file looks ` +
|
|
230
|
+
`truncated, so any tags after the cut were not seen`;
|
|
231
|
+
}
|
|
232
|
+
else if (unbalancedClose)
|
|
233
|
+
reason = 'unbalanced `}`/`]` in the file, so its object structure could not be tracked';
|
|
234
|
+
else if (!sawTablesMap)
|
|
235
|
+
reason = 'no `tables:` map found; this does not look like generated Turbine metadata';
|
|
236
|
+
else if (columnsSeen === 0)
|
|
237
|
+
reason = 'found a `tables:` map but no parsable column objects inside it';
|
|
238
|
+
return { tags, ok: reason === undefined, reason, tablesSeen, columnsSeen };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Tag map only. Kept for callers that already treat "no tags" as safe; anything
|
|
242
|
+
* that redacts should prefer {@link scanPiiTags} so it can see a failed scan.
|
|
243
|
+
*/
|
|
244
|
+
function parsePiiTags(source) {
|
|
245
|
+
return scanPiiTags(source).tags;
|
|
71
246
|
}
|
|
72
247
|
/**
|
|
73
248
|
* Read PII tags from the generated metadata in `outDir`, or return `null` when
|
|
74
249
|
* there is no readable generated metadata there. Never throws.
|
|
250
|
+
*
|
|
251
|
+
* A returned source with `scan.ok === false` means a file WAS found and could
|
|
252
|
+
* not be understood: that is strictly worse than no file at all, because the
|
|
253
|
+
* user believes their tags are in force. Callers must not treat it as "no PII".
|
|
75
254
|
*/
|
|
76
255
|
function loadPiiTags(outDir) {
|
|
77
256
|
for (const file of ['metadata.ts', 'metadata.js']) {
|
|
@@ -83,9 +262,9 @@ function loadPiiTags(outDir) {
|
|
|
83
262
|
catch {
|
|
84
263
|
continue;
|
|
85
264
|
}
|
|
86
|
-
const
|
|
87
|
-
const count = Object.values(tags).reduce((n, cols) => n + cols.length, 0);
|
|
88
|
-
return { path, tags, count };
|
|
265
|
+
const scan = scanPiiTags(source);
|
|
266
|
+
const count = Object.values(scan.tags).reduce((n, cols) => n + cols.length, 0);
|
|
267
|
+
return { path, tags: scan.tags, count, scan };
|
|
89
268
|
}
|
|
90
269
|
return null;
|
|
91
270
|
}
|