@hypequery/cli 1.17.0 → 1.18.1
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/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/commands/generate-datasets.d.ts +1 -0
- package/dist/commands/generate-datasets.d.ts.map +1 -1
- package/dist/commands/generate-datasets.js +5 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -4
- package/dist/generators/dataset-generator.d.ts +21 -0
- package/dist/generators/dataset-generator.d.ts.map +1 -1
- package/dist/generators/dataset-generator.js +52 -12
- package/dist/templates/auth-scaffold.d.ts +6 -0
- package/dist/templates/auth-scaffold.d.ts.map +1 -1
- package/dist/templates/auth-scaffold.js +7 -1
- package/dist/templates/datasets.d.ts +4 -1
- package/dist/templates/datasets.d.ts.map +1 -1
- package/dist/templates/datasets.js +13 -3
- package/dist/typegen/generate-types.d.ts.map +1 -1
- package/dist/typegen/generate-types.js +2 -3
- package/dist/typegen/type-parsing.d.ts.map +1 -1
- package/dist/typegen/type-parsing.js +77 -6
- package/dist/utils/dataset-generation-warnings.d.ts +3 -0
- package/dist/utils/dataset-generation-warnings.d.ts.map +1 -0
- package/dist/utils/dataset-generation-warnings.js +11 -0
- package/dist/utils/regenerate-datasets-command.d.ts +16 -0
- package/dist/utils/regenerate-datasets-command.d.ts.map +1 -0
- package/dist/utils/regenerate-datasets-command.js +18 -0
- package/package.json +2 -2
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAgQD,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -142,6 +142,7 @@ program
|
|
|
142
142
|
.option('--path <path>', 'Analytics directory (derives <path>/datasets.ts)')
|
|
143
143
|
.option('--tables <names>', 'Only generate for specific tables (comma-separated)')
|
|
144
144
|
.option('--exclude-tables <names>', 'Exclude specific tables (comma-separated)')
|
|
145
|
+
.option('--tenant-column <column>', 'Set tenantKey to this column on tables that have it (requires a trusted runtime tenant scope)')
|
|
145
146
|
.action(runCommand(async (options) => {
|
|
146
147
|
await generateDatasetsCommand(options);
|
|
147
148
|
}));
|
|
@@ -9,6 +9,7 @@ export interface GenerateDatasetsOptions {
|
|
|
9
9
|
path?: string;
|
|
10
10
|
tables?: string;
|
|
11
11
|
excludeTables?: string;
|
|
12
|
+
tenantColumn?: string;
|
|
12
13
|
}
|
|
13
14
|
export declare function generateDatasetsCommand(options?: GenerateDatasetsOptions): Promise<void>;
|
|
14
15
|
//# sourceMappingURL=generate-datasets.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-datasets.d.ts","sourceRoot":"","sources":["../../src/commands/generate-datasets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"generate-datasets.d.ts","sourceRoot":"","sources":["../../src/commands/generate-datasets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAqBD,wBAAsB,uBAAuB,CAAC,OAAO,GAAE,uBAA4B,iBA8GlF"}
|
|
@@ -10,6 +10,7 @@ import { logger } from '../utils/logger.js';
|
|
|
10
10
|
import { getTableCount } from '../utils/detect-database.js';
|
|
11
11
|
import { generateDatasets } from '../generators/dataset-generator.js';
|
|
12
12
|
import { redactConnectionUrl } from '../utils/redact-connection-url.js';
|
|
13
|
+
import { logDatasetGenerationWarnings } from '../utils/dataset-generation-warnings.js';
|
|
13
14
|
/**
|
|
14
15
|
* Turns the written path into something pasteable into an import statement:
|
|
15
16
|
* strips the `.ts` extension and makes it explicitly relative.
|
|
@@ -58,12 +59,16 @@ export async function generateDatasetsCommand(options = {}) {
|
|
|
58
59
|
outputPath,
|
|
59
60
|
includeTables: parsedTables,
|
|
60
61
|
excludeTables: excludedTables,
|
|
62
|
+
// Regeneration replaces the whole file, so a tenant boundary configured at
|
|
63
|
+
// init has to be restated here or it is dropped on every refresh.
|
|
64
|
+
tenantColumn: options.tenantColumn,
|
|
61
65
|
});
|
|
62
66
|
const generatedTables = generated?.tables ?? parsedTables ?? [];
|
|
63
67
|
const generatedCount = generatedTables.length || tableCount;
|
|
64
68
|
datasetSpinner.succeed(`Generated dataset definitions for ${generatedCount} ${generatedCount === 1 ? 'table' : 'tables'}`);
|
|
65
69
|
const relativeOutput = path.relative(process.cwd(), outputPath);
|
|
66
70
|
logger.success(`Created ${relativeOutput}`);
|
|
71
|
+
logDatasetGenerationWarnings(generated?.warnings);
|
|
67
72
|
logger.newline();
|
|
68
73
|
logger.header('Next steps:');
|
|
69
74
|
logger.indent('1. Review and customize the generated datasets');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAIA,OAAO,EAcL,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAe7B,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAIA,OAAO,EAcL,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAe7B,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAWzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAqKD,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,iBAkb1D"}
|
package/dist/commands/init.js
CHANGED
|
@@ -10,11 +10,14 @@ import { generateEnvTemplate, appendToEnv } from '../templates/env.js';
|
|
|
10
10
|
import { generateClientTemplate } from '../templates/client.js';
|
|
11
11
|
import { generateQueriesTemplate } from '../templates/queries.js';
|
|
12
12
|
import { generateApiTemplate } from '../templates/api.js';
|
|
13
|
+
import { CONTEXT_AUTH_TENANT_COLUMN } from '../templates/auth-scaffold.js';
|
|
13
14
|
import { generateDatasetsPlaceholderTemplate } from '../templates/datasets.js';
|
|
14
15
|
import { appendToGitignore } from '../templates/gitignore.js';
|
|
15
16
|
import { getTypeGenerator } from '../generators/index.js';
|
|
16
17
|
import { generateDatasets } from '../generators/dataset-generator.js';
|
|
17
18
|
import { installScaffoldDependencies } from '../utils/dependency-installer.js';
|
|
19
|
+
import { logDatasetGenerationWarnings } from '../utils/dataset-generation-warnings.js';
|
|
20
|
+
import { formatRegenerateDatasetsCommand } from '../utils/regenerate-datasets-command.js';
|
|
18
21
|
function normalizeInitDatabase(database) {
|
|
19
22
|
if (!database || database === 'clickhouse') {
|
|
20
23
|
return 'clickhouse';
|
|
@@ -389,22 +392,29 @@ export interface IntrospectedSchema {
|
|
|
389
392
|
const shouldGenerateDatasets = hasValidConnection && (options.allTables === true ||
|
|
390
393
|
(datasetTables !== undefined && datasetTables.length > 0));
|
|
391
394
|
if (shouldGenerateDatasets) {
|
|
392
|
-
await generateDatasets({
|
|
395
|
+
const generated = await generateDatasets({
|
|
393
396
|
outputPath: datasetsPath,
|
|
394
397
|
includeTables: options.allTables ? undefined : datasetTables,
|
|
395
398
|
excludeTables: excludedDatasetTables,
|
|
399
|
+
// Context auth scaffolds a trusted runtime tenant scope on that column,
|
|
400
|
+
// so datasets must declare the matching tenantKey or every tenant-scoped
|
|
401
|
+
// request fails. This is explicit configuration, not a name heuristic.
|
|
402
|
+
...(auth === 'context' ? { tenantColumn: CONTEXT_AUTH_TENANT_COLUMN } : {}),
|
|
396
403
|
...(database === 'chdb'
|
|
397
404
|
? { client: getChdbTypeGenerationClient(chdbPath) }
|
|
398
405
|
: {}),
|
|
399
406
|
});
|
|
407
|
+
logDatasetGenerationWarnings(generated?.warnings);
|
|
400
408
|
generatedAnyDatasets = true;
|
|
401
409
|
generatedSelectedDataset = selectedTable !== null && (options.allTables === true ||
|
|
402
410
|
datasetTables?.includes(selectedTable) === true);
|
|
403
411
|
}
|
|
404
412
|
else {
|
|
405
|
-
await writeFile(datasetsPath, generateDatasetsPlaceholderTemplate());
|
|
413
|
+
await writeFile(datasetsPath, generateDatasetsPlaceholderTemplate({ auth }));
|
|
406
414
|
if (hasValidConnection) {
|
|
407
|
-
logger.info('Skipped dataset generation. Run `
|
|
415
|
+
logger.info('Skipped dataset generation. Run `'
|
|
416
|
+
+ formatRegenerateDatasetsCommand({ outputDir, auth, tables: 'table1,table2' })
|
|
417
|
+
+ '` when ready.');
|
|
408
418
|
}
|
|
409
419
|
}
|
|
410
420
|
logger.success(`Created datasets file (${path.relative(process.cwd(), datasetsPath)})`);
|
|
@@ -451,7 +461,7 @@ export interface IntrospectedSchema {
|
|
|
451
461
|
if (hasValidConnection) {
|
|
452
462
|
if (style === 'datasets' && !generatedAnyDatasets) {
|
|
453
463
|
logger.info('Next:');
|
|
454
|
-
logger.indent(
|
|
464
|
+
logger.indent(formatRegenerateDatasetsCommand({ outputDir, auth, tables: 'table1,table2' }));
|
|
455
465
|
logger.newline();
|
|
456
466
|
}
|
|
457
467
|
else if (style === 'datasets' && !generatedSelectedDataset) {
|
|
@@ -10,7 +10,27 @@ export interface DatasetGeneratorOptions {
|
|
|
10
10
|
includeTables?: string[];
|
|
11
11
|
excludeTables?: string[];
|
|
12
12
|
client?: TypeGenerationClickHouseClient;
|
|
13
|
+
/**
|
|
14
|
+
* Tenant column the caller has already configured as a trusted runtime tenant
|
|
15
|
+
* scope (for example the `tenant.column` of a scaffolded context-auth API).
|
|
16
|
+
*
|
|
17
|
+
* Only an explicit value here activates `tenantKey`; column-name heuristics
|
|
18
|
+
* never do. Tables that lack the column are reported as warnings instead of
|
|
19
|
+
* being silently left unscoped.
|
|
20
|
+
*/
|
|
21
|
+
tenantColumn?: string;
|
|
13
22
|
}
|
|
23
|
+
export type DatasetGenerationWarning = {
|
|
24
|
+
kind: 'tenant-key-candidate';
|
|
25
|
+
table: string;
|
|
26
|
+
columns: string[];
|
|
27
|
+
message: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'tenant-column-missing';
|
|
30
|
+
table: string;
|
|
31
|
+
column: string;
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
14
34
|
/**
|
|
15
35
|
* Generate dataset definitions from ClickHouse schema
|
|
16
36
|
*/
|
|
@@ -20,5 +40,6 @@ export declare function generateDatasets(options: DatasetGeneratorOptions): Prom
|
|
|
20
40
|
table: string;
|
|
21
41
|
exportName: string;
|
|
22
42
|
}[];
|
|
43
|
+
warnings: DatasetGenerationWarning[];
|
|
23
44
|
}>;
|
|
24
45
|
//# sourceMappingURL=dataset-generator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-generator.d.ts","sourceRoot":"","sources":["../../src/generators/dataset-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAC;AAG1E,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"dataset-generator.d.ts","sourceRoot":"","sources":["../../src/generators/dataset-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAC;AAG1E,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,8BAA8B,CAAC;IACxC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,wBAAwB,GAChC;IACE,IAAI,EAAE,sBAAsB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,uBAAuB,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAkSN;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB;;;;;;;GAsEtE"}
|
|
@@ -48,9 +48,12 @@ function isTimestampColumn(column) {
|
|
|
48
48
|
name === 'updated');
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Check if a column is
|
|
51
|
+
* Check if a column name is worth presenting as a possible tenant key.
|
|
52
|
+
*
|
|
53
|
+
* A naming match is never enough to activate tenant enforcement. `tenantKey`
|
|
54
|
+
* is a security boundary that must be chosen explicitly by the application.
|
|
52
55
|
*/
|
|
53
|
-
function
|
|
56
|
+
function isTenantKeyCandidate(column) {
|
|
54
57
|
const name = column.name.toLowerCase();
|
|
55
58
|
return (name === 'tenant_id' ||
|
|
56
59
|
name === 'organization_id' ||
|
|
@@ -79,7 +82,7 @@ function isIdentifierColumn(column) {
|
|
|
79
82
|
name.endsWith('_id') ||
|
|
80
83
|
originalName.endsWith('Id') ||
|
|
81
84
|
name.includes('uuid') ||
|
|
82
|
-
|
|
85
|
+
isTenantKeyCandidate(column));
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
85
88
|
* Check if a numeric column is a coordinate that should not be summed or averaged.
|
|
@@ -128,7 +131,7 @@ function generateLabel(columnName) {
|
|
|
128
131
|
/**
|
|
129
132
|
* Generate dataset definition code for a single table
|
|
130
133
|
*/
|
|
131
|
-
async function generateDatasetForTable(client, tableName) {
|
|
134
|
+
async function generateDatasetForTable(client, tableName, tenantColumn) {
|
|
132
135
|
// Get column information
|
|
133
136
|
const columnsQuery = await client.query({
|
|
134
137
|
query: `DESCRIBE TABLE ${tableName}`,
|
|
@@ -138,8 +141,34 @@ async function generateDatasetForTable(client, tableName) {
|
|
|
138
141
|
// Determine timeKey (prefer created_at, then any timestamp column)
|
|
139
142
|
const timestampColumns = columns.filter(isTimestampColumn);
|
|
140
143
|
const timeKeyColumn = timestampColumns.find((c) => c.name === 'created_at') || timestampColumns[0];
|
|
141
|
-
//
|
|
142
|
-
|
|
144
|
+
// A tenant column is only honoured when the caller configured one explicitly.
|
|
145
|
+
// Column-name matches are reported for review and never become policy.
|
|
146
|
+
const resolvedTenantColumn = tenantColumn
|
|
147
|
+
? columns.find((column) => column.name === tenantColumn)?.name
|
|
148
|
+
: undefined;
|
|
149
|
+
// Report every match, not just the first — picking the right tenant boundary
|
|
150
|
+
// needs the full set (e.g. both `organization_id` and `customer_id`).
|
|
151
|
+
const tenantCandidates = columns.filter(isTenantKeyCandidate).map((column) => column.name);
|
|
152
|
+
const warnings = [];
|
|
153
|
+
if (tenantColumn && !resolvedTenantColumn) {
|
|
154
|
+
warnings.push({
|
|
155
|
+
kind: 'tenant-column-missing',
|
|
156
|
+
table: tableName,
|
|
157
|
+
column: tenantColumn,
|
|
158
|
+
message: `Dataset "${tableName}" has no "${tenantColumn}" column, so tenant isolation was not applied. ` +
|
|
159
|
+
'Tenant-scoped requests against it will fail until you set tenantKey to the correct column.',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (!resolvedTenantColumn && tenantCandidates.length > 0) {
|
|
163
|
+
const quoted = tenantCandidates.map((name) => `"${name}"`).join(', ');
|
|
164
|
+
warnings.push({
|
|
165
|
+
kind: 'tenant-key-candidate',
|
|
166
|
+
table: tableName,
|
|
167
|
+
columns: tenantCandidates,
|
|
168
|
+
message: `Dataset "${tableName}" has ${tenantCandidates.length === 1 ? 'possible tenant key' : 'possible tenant keys'} ${quoted}. ` +
|
|
169
|
+
'Review them and add tenantKey explicitly only after configuring trusted runtime tenant scope.',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
143
172
|
// Generate dimensions
|
|
144
173
|
const dimensionLines = [];
|
|
145
174
|
for (const column of columns) {
|
|
@@ -181,8 +210,13 @@ async function generateDatasetForTable(client, tableName) {
|
|
|
181
210
|
if (timeKeyColumn) {
|
|
182
211
|
configLines.push(` timeKey: '${timeKeyColumn.name}',`);
|
|
183
212
|
}
|
|
184
|
-
if (
|
|
185
|
-
configLines.push(` tenantKey: '${
|
|
213
|
+
if (resolvedTenantColumn) {
|
|
214
|
+
configLines.push(` tenantKey: '${resolvedTenantColumn}',`);
|
|
215
|
+
}
|
|
216
|
+
else if (tenantCandidates.length > 0) {
|
|
217
|
+
const quoted = tenantCandidates.map((name) => `'${name}'`).join(', ');
|
|
218
|
+
configLines.push(` // Possible tenant ${tenantCandidates.length === 1 ? 'key' : 'keys'}: ${quoted}.`);
|
|
219
|
+
configLines.push(` // Add tenantKey explicitly only after configuring trusted runtime tenant scope.`);
|
|
186
220
|
}
|
|
187
221
|
configLines.push(` dimensions: {`);
|
|
188
222
|
configLines.push(...dimensionLines);
|
|
@@ -190,10 +224,13 @@ async function generateDatasetForTable(client, tableName) {
|
|
|
190
224
|
configLines.push(` measures: {`);
|
|
191
225
|
configLines.push(...measureLines);
|
|
192
226
|
configLines.push(` },`);
|
|
193
|
-
return
|
|
227
|
+
return {
|
|
228
|
+
code: `export const ${datasetName}Dataset = dataset('${tableName}', {
|
|
194
229
|
${configLines.join('\n')}
|
|
195
230
|
});
|
|
196
|
-
|
|
231
|
+
`,
|
|
232
|
+
warnings,
|
|
233
|
+
};
|
|
197
234
|
}
|
|
198
235
|
/**
|
|
199
236
|
* Generate dataset definitions from ClickHouse schema
|
|
@@ -220,9 +257,11 @@ export async function generateDatasets(options) {
|
|
|
220
257
|
}
|
|
221
258
|
// Generate dataset definitions
|
|
222
259
|
const datasetDefinitions = [];
|
|
260
|
+
const warnings = [];
|
|
223
261
|
for (const table of tables) {
|
|
224
|
-
const
|
|
225
|
-
datasetDefinitions.push(
|
|
262
|
+
const generated = await generateDatasetForTable(client, table.name, options.tenantColumn);
|
|
263
|
+
datasetDefinitions.push(generated.code);
|
|
264
|
+
warnings.push(...generated.warnings);
|
|
226
265
|
}
|
|
227
266
|
// Build the complete output file
|
|
228
267
|
const header = `// Generated by @hypequery/cli
|
|
@@ -251,5 +290,6 @@ ${tables.map((table) => ` ${table.name}: ${tableToPascalCase(table.name)}Datase
|
|
|
251
290
|
table: table.name,
|
|
252
291
|
exportName: `${tableToPascalCase(table.name)}Dataset`,
|
|
253
292
|
})),
|
|
293
|
+
warnings,
|
|
254
294
|
};
|
|
255
295
|
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
* templates so the generated code stays identical across scaffold styles.
|
|
4
4
|
*/
|
|
5
5
|
export type AuthTemplateMode = 'none' | 'context';
|
|
6
|
+
/**
|
|
7
|
+
* Tenant column the context-auth scaffold configures as its trusted runtime
|
|
8
|
+
* tenant scope. Generated datasets must use the same column for their
|
|
9
|
+
* `tenantKey`, otherwise tenant-scoped requests fail at runtime.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CONTEXT_AUTH_TENANT_COLUMN = "tenant_id";
|
|
6
12
|
/**
|
|
7
13
|
* Type guards that pull a trusted `HostUser` off the underlying request. The
|
|
8
14
|
* host framework is expected to have already authenticated the request and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-scaffold.d.ts","sourceRoot":"","sources":["../../src/templates/auth-scaffold.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,eAAe,+qBAoBzB,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,iBAAiB,gVAUzB,CAAC"}
|
|
1
|
+
{"version":3,"file":"auth-scaffold.d.ts","sourceRoot":"","sources":["../../src/templates/auth-scaffold.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,cAAc,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,eAAe,+qBAoBzB,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,iBAAiB,gVAUzB,CAAC"}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant column the context-auth scaffold configures as its trusted runtime
|
|
3
|
+
* tenant scope. Generated datasets must use the same column for their
|
|
4
|
+
* `tenantKey`, otherwise tenant-scoped requests fail at runtime.
|
|
5
|
+
*/
|
|
6
|
+
export const CONTEXT_AUTH_TENANT_COLUMN = 'tenant_id';
|
|
1
7
|
/**
|
|
2
8
|
* Type guards that pull a trusted `HostUser` off the underlying request. The
|
|
3
9
|
* host framework is expected to have already authenticated the request and
|
|
@@ -36,6 +42,6 @@ export const contextAuthConfig = ` auth: fromContext(({ request }) => {
|
|
|
36
42
|
}),
|
|
37
43
|
tenant: {
|
|
38
44
|
extract: (auth: { tenantId?: string }) => auth.tenantId,
|
|
39
|
-
column: '
|
|
45
|
+
column: '${CONTEXT_AUTH_TENANT_COLUMN}',
|
|
40
46
|
mode: 'auto-inject',
|
|
41
47
|
},`;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { type AuthTemplateMode } from './auth-scaffold.js';
|
|
1
2
|
/**
|
|
2
3
|
* Generate placeholder datasets.ts file for no-connection init.
|
|
3
4
|
*/
|
|
4
|
-
export declare function generateDatasetsPlaceholderTemplate(
|
|
5
|
+
export declare function generateDatasetsPlaceholderTemplate(options?: {
|
|
6
|
+
auth?: AuthTemplateMode;
|
|
7
|
+
}): string;
|
|
5
8
|
//# sourceMappingURL=datasets.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"datasets.d.ts","sourceRoot":"","sources":["../../src/templates/datasets.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,mCAAmC,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"datasets.d.ts","sourceRoot":"","sources":["../../src/templates/datasets.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,oBAAoB,CAAC;AAE5B;;GAEG;AACH,wBAAgB,mCAAmC,CACjD,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,gBAAgB,CAAA;CAAO,GACxC,MAAM,CA6BR"}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
import { CONTEXT_AUTH_TENANT_COLUMN, } from './auth-scaffold.js';
|
|
1
2
|
/**
|
|
2
3
|
* Generate placeholder datasets.ts file for no-connection init.
|
|
3
4
|
*/
|
|
4
|
-
export function generateDatasetsPlaceholderTemplate() {
|
|
5
|
+
export function generateDatasetsPlaceholderTemplate(options = {}) {
|
|
6
|
+
// Context auth wires a trusted runtime tenant scope in api.ts, and tenant-scoped
|
|
7
|
+
// dataset requests fail unless the dataset declares the matching tenantKey.
|
|
8
|
+
const tenantScoped = options.auth === 'context';
|
|
9
|
+
const tenantKeyLine = tenantScoped
|
|
10
|
+
? ` tenantKey: '${CONTEXT_AUTH_TENANT_COLUMN}',\n`
|
|
11
|
+
: '';
|
|
12
|
+
const tenantDimensionLine = tenantScoped
|
|
13
|
+
? ` tenantId: dimension.string({ column: '${CONTEXT_AUTH_TENANT_COLUMN}' }),\n`
|
|
14
|
+
: '';
|
|
5
15
|
return `import { dataset, dimension, measure } from '@hypequery/datasets';
|
|
6
16
|
|
|
7
17
|
const exampleEvents = dataset('example_events', {
|
|
8
18
|
source: 'example_events',
|
|
9
19
|
timeKey: 'created_at',
|
|
10
|
-
dimensions: {
|
|
20
|
+
${tenantKeyLine} dimensions: {
|
|
11
21
|
id: dimension.string(),
|
|
12
|
-
createdAt: dimension.timestamp({ column: 'created_at' }),
|
|
22
|
+
${tenantDimensionLine} createdAt: dimension.timestamp({ column: 'created_at' }),
|
|
13
23
|
},
|
|
14
24
|
measures: {
|
|
15
25
|
eventCount: measure.count('id', { label: 'Event Count' }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-types.d.ts","sourceRoot":"","sources":["../../src/typegen/generate-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,GAAG,OAAO,CAAC;QAChE,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;KAChD,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,qCAAqC;IACrC,MAAM,EAAE,8BAA8B,CAAC;IACvC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;
|
|
1
|
+
{"version":3,"file":"generate-types.d.ts","sourceRoot":"","sources":["../../src/typegen/generate-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,GAAG,OAAO,CAAC;QAChE,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;KAChD,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,qCAAqC;IACrC,MAAM,EAAE,8BAA8B,CAAC;IACvC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AA0HD;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,8BAA8B,EACtC,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAM,GACjD,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,MAAM,EAAE,CAAC,CAYnB"}
|
|
@@ -61,8 +61,7 @@ export interface IntrospectedSchema {`;
|
|
|
61
61
|
const columns = await describeTable(client, table.name);
|
|
62
62
|
typeDefinitions += `\n ${table.name}: {`;
|
|
63
63
|
for (const column of columns) {
|
|
64
|
-
|
|
65
|
-
typeDefinitions += `\n '${column.name}': '${clickHouseType}';`;
|
|
64
|
+
typeDefinitions += `\n '${column.name}': ${JSON.stringify(column.type)};`;
|
|
66
65
|
}
|
|
67
66
|
typeDefinitions += '\n };';
|
|
68
67
|
}
|
|
@@ -73,7 +72,7 @@ export interface IntrospectedSchema {`;
|
|
|
73
72
|
const columns = await describeTable(client, table.name);
|
|
74
73
|
typeDefinitions += `export interface ${capitalizeFirstLetter(table.name)}Record {`;
|
|
75
74
|
for (const column of columns) {
|
|
76
|
-
const tsType = clickhouseToTsType(column.type)
|
|
75
|
+
const tsType = clickhouseToTsType(column.type);
|
|
77
76
|
typeDefinitions += `\n '${column.name}': ${tsType};`;
|
|
78
77
|
}
|
|
79
78
|
typeDefinitions += '\n}\n\n';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type-parsing.d.ts","sourceRoot":"","sources":["../../src/typegen/type-parsing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"type-parsing.d.ts","sourceRoot":"","sources":["../../src/typegen/type-parsing.ts"],"names":[],"mappings":"AAwLA;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,MA6CjD,CAAC"}
|
|
@@ -2,7 +2,27 @@ function splitTopLevelArgs(value) {
|
|
|
2
2
|
const parts = [];
|
|
3
3
|
let current = '';
|
|
4
4
|
let depth = 0;
|
|
5
|
+
let quote = null;
|
|
6
|
+
let escaped = false;
|
|
5
7
|
for (const char of value) {
|
|
8
|
+
if (quote) {
|
|
9
|
+
current += char;
|
|
10
|
+
if (escaped) {
|
|
11
|
+
escaped = false;
|
|
12
|
+
}
|
|
13
|
+
else if (char === '\\') {
|
|
14
|
+
escaped = true;
|
|
15
|
+
}
|
|
16
|
+
else if (char === quote) {
|
|
17
|
+
quote = null;
|
|
18
|
+
}
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
22
|
+
quote = char;
|
|
23
|
+
current += char;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
6
26
|
if (char === '(') {
|
|
7
27
|
depth += 1;
|
|
8
28
|
current += char;
|
|
@@ -25,6 +45,50 @@ function splitTopLevelArgs(value) {
|
|
|
25
45
|
}
|
|
26
46
|
return parts;
|
|
27
47
|
}
|
|
48
|
+
function decodeClickHouseEscape(char) {
|
|
49
|
+
switch (char) {
|
|
50
|
+
case 'b': return '\b';
|
|
51
|
+
case 'f': return '\f';
|
|
52
|
+
case 'n': return '\n';
|
|
53
|
+
case 'r': return '\r';
|
|
54
|
+
case 't': return '\t';
|
|
55
|
+
case '0': return '\0';
|
|
56
|
+
default: return char;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function parseBacktickTuplePart(part) {
|
|
60
|
+
let name = '';
|
|
61
|
+
let escaped = false;
|
|
62
|
+
for (let index = 1; index < part.length; index += 1) {
|
|
63
|
+
const char = part[index];
|
|
64
|
+
if (escaped) {
|
|
65
|
+
name += decodeClickHouseEscape(char);
|
|
66
|
+
escaped = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (char === '\\') {
|
|
70
|
+
escaped = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (char === '`') {
|
|
74
|
+
const type = part.slice(index + 1);
|
|
75
|
+
return /^\s+\S/s.test(type) ? { name, type: type.trim() } : null;
|
|
76
|
+
}
|
|
77
|
+
name += char;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function parseNamedTuplePart(value) {
|
|
82
|
+
const part = value.trim();
|
|
83
|
+
if (part.startsWith('`')) {
|
|
84
|
+
return parseBacktickTuplePart(part);
|
|
85
|
+
}
|
|
86
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)\s+(.+)$/s.exec(part);
|
|
87
|
+
return match ? { name: match[1], type: match[2].trim() } : null;
|
|
88
|
+
}
|
|
89
|
+
function formatTypeScriptProperty(name) {
|
|
90
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
91
|
+
}
|
|
28
92
|
function unwrapType(type, wrapperName) {
|
|
29
93
|
// Trim whitespace and handle case-insensitive matching
|
|
30
94
|
const trimmedType = type.trim();
|
|
@@ -104,24 +168,31 @@ function getPrimitiveTsType(type) {
|
|
|
104
168
|
* equivalent TypeScript type string.
|
|
105
169
|
*/
|
|
106
170
|
export const clickhouseToTsType = (type) => {
|
|
107
|
-
const
|
|
171
|
+
const normalizedType = type.trim();
|
|
172
|
+
const wrappedArrayType = unwrapType(normalizedType, 'Array');
|
|
108
173
|
if (wrappedArrayType) {
|
|
109
174
|
return `Array<${clickhouseToTsType(wrappedArrayType)}>`;
|
|
110
175
|
}
|
|
111
|
-
const wrappedNullableType = unwrapType(
|
|
176
|
+
const wrappedNullableType = unwrapType(normalizedType, 'Nullable');
|
|
112
177
|
if (wrappedNullableType) {
|
|
113
178
|
return `${clickhouseToTsType(wrappedNullableType)} | null`;
|
|
114
179
|
}
|
|
115
|
-
const wrappedLowCardinalityType = unwrapType(
|
|
180
|
+
const wrappedLowCardinalityType = unwrapType(normalizedType, 'LowCardinality');
|
|
116
181
|
if (wrappedLowCardinalityType) {
|
|
117
182
|
return clickhouseToTsType(wrappedLowCardinalityType);
|
|
118
183
|
}
|
|
119
|
-
const wrappedTupleType = unwrapType(
|
|
184
|
+
const wrappedTupleType = unwrapType(normalizedType, 'Tuple');
|
|
120
185
|
if (wrappedTupleType) {
|
|
121
186
|
const tupleParts = splitTopLevelArgs(wrappedTupleType);
|
|
187
|
+
const namedParts = tupleParts.map(parseNamedTuplePart);
|
|
188
|
+
if (namedParts.length > 0 && namedParts.every((part) => part !== null)) {
|
|
189
|
+
return `{ ${namedParts
|
|
190
|
+
.map((part) => `${formatTypeScriptProperty(part.name)}: ${clickhouseToTsType(part.type)}`)
|
|
191
|
+
.join('; ')} }`;
|
|
192
|
+
}
|
|
122
193
|
return `[${tupleParts.map((part) => clickhouseToTsType(part)).join(', ')}]`;
|
|
123
194
|
}
|
|
124
|
-
const wrappedMapType = unwrapType(
|
|
195
|
+
const wrappedMapType = unwrapType(normalizedType, 'Map');
|
|
125
196
|
if (wrappedMapType) {
|
|
126
197
|
const mapParts = splitTopLevelArgs(wrappedMapType);
|
|
127
198
|
if (mapParts.length === 2) {
|
|
@@ -131,7 +202,7 @@ export const clickhouseToTsType = (type) => {
|
|
|
131
202
|
}
|
|
132
203
|
return 'Record<string, unknown>';
|
|
133
204
|
}
|
|
134
|
-
const primitiveType = getPrimitiveTsType(
|
|
205
|
+
const primitiveType = getPrimitiveTsType(normalizedType);
|
|
135
206
|
if (primitiveType)
|
|
136
207
|
return primitiveType;
|
|
137
208
|
// Unsupported or more complex ClickHouse types currently preserve the historical fallback.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-generation-warnings.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-generation-warnings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAGnF,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,SAAS,wBAAwB,EAAE,GAAG,SAAS,GACxD,IAAI,CAUN"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
export function logDatasetGenerationWarnings(warnings) {
|
|
3
|
+
if (!warnings?.length) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
logger.newline();
|
|
7
|
+
logger.warn('Review generated dataset tenant isolation:');
|
|
8
|
+
for (const warning of warnings) {
|
|
9
|
+
logger.indent(`• ${warning.message}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AuthTemplateMode } from '../templates/auth-scaffold.js';
|
|
2
|
+
export interface RegenerateDatasetsCommandOptions {
|
|
3
|
+
outputDir: string;
|
|
4
|
+
auth: AuthTemplateMode;
|
|
5
|
+
/** Comma-separated table list to suggest, if any. */
|
|
6
|
+
tables?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The `generate:datasets` invocation to hand a user after `init`.
|
|
10
|
+
*
|
|
11
|
+
* Regeneration rewrites the whole file, so a context-auth scaffold has to
|
|
12
|
+
* repeat its tenant column here. Without it the refresh drops `tenantKey` and
|
|
13
|
+
* every tenant-scoped request starts failing.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatRegenerateDatasetsCommand(options: RegenerateDatasetsCommandOptions): string;
|
|
16
|
+
//# sourceMappingURL=regenerate-datasets-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regenerate-datasets-command.d.ts","sourceRoot":"","sources":["../../src/utils/regenerate-datasets-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAElG,MAAM,WAAW,gCAAgC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,gBAAgB,CAAC;IACvB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,gCAAgC,GACxC,MAAM,CAYR"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CONTEXT_AUTH_TENANT_COLUMN } from '../templates/auth-scaffold.js';
|
|
2
|
+
/**
|
|
3
|
+
* The `generate:datasets` invocation to hand a user after `init`.
|
|
4
|
+
*
|
|
5
|
+
* Regeneration rewrites the whole file, so a context-auth scaffold has to
|
|
6
|
+
* repeat its tenant column here. Without it the refresh drops `tenantKey` and
|
|
7
|
+
* every tenant-scoped request starts failing.
|
|
8
|
+
*/
|
|
9
|
+
export function formatRegenerateDatasetsCommand(options) {
|
|
10
|
+
const parts = ['hypequery generate:datasets', `--path ${options.outputDir}`];
|
|
11
|
+
if (options.tables) {
|
|
12
|
+
parts.push(`--tables ${options.tables}`);
|
|
13
|
+
}
|
|
14
|
+
if (options.auth === 'context') {
|
|
15
|
+
parts.push(`--tenant-column ${CONTEXT_AUTH_TENANT_COLUMN}`);
|
|
16
|
+
}
|
|
17
|
+
return parts.join(' ');
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.1",
|
|
4
4
|
"description": "CLI for scaffolding type-safe ClickHouse analytics, semantic datasets, React manifests, and deployments",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"clickhouse",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@vitest/coverage-v8": "^3.2.6",
|
|
46
46
|
"typescript": "^5.7.3",
|
|
47
47
|
"vitest": "^3.2.6",
|
|
48
|
-
"@hypequery/serve": "0.16.
|
|
48
|
+
"@hypequery/serve": "0.16.1"
|
|
49
49
|
},
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|