@hypequery/cli 1.26.0 → 1.27.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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +17 -1
- package/dist/commands/generate-client.d.ts.map +1 -1
- package/dist/commands/generate-client.js +9 -2
- package/dist/generators/dataset-generator.d.ts.map +1 -1
- package/dist/generators/dataset-generator.js +75 -56
- package/dist/utils/clickhouse-dimension-type.d.ts +27 -0
- package/dist/utils/clickhouse-dimension-type.d.ts.map +1 -0
- package/dist/utils/clickhouse-dimension-type.js +61 -0
- package/dist/utils/clickhouse-type-utils.d.ts.map +1 -1
- package/dist/utils/clickhouse-type-utils.js +9 -1
- package/dist/utils/client-contract-file.d.ts +5 -0
- package/dist/utils/client-contract-file.d.ts.map +1 -1
- package/dist/utils/client-contract-file.js +49 -5
- package/dist/utils/deployment-upload.d.ts +6 -2
- package/dist/utils/deployment-upload.d.ts.map +1 -1
- package/dist/utils/deployment-upload.js +12 -2
- package/dist/utils/draft-deployment.d.ts +16 -0
- package/dist/utils/draft-deployment.d.ts.map +1 -0
- package/dist/utils/draft-deployment.js +35 -0
- package/dist/utils/field-namer.d.ts +10 -0
- package/dist/utils/field-namer.d.ts.map +1 -0
- package/dist/utils/field-namer.js +20 -0
- package/dist/utils/json-pointer.d.ts +3 -0
- package/dist/utils/json-pointer.d.ts.map +1 -0
- package/dist/utils/json-pointer.js +4 -0
- package/dist/utils/telemetry/catalog.d.ts +1 -1
- package/dist/utils/telemetry/domains.d.ts +4 -4
- package/dist/utils/telemetry/domains.js +4 -4
- package/package.json +5 -5
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0CpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAE9B,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":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0CpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAE9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AA2SD,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -208,6 +208,7 @@ program
|
|
|
208
208
|
.command('deployment:submit <bundle>')
|
|
209
209
|
.description('Submit a prebuilt deployment bundle and release')
|
|
210
210
|
.requiredOption('--release <path>', 'Target-bound release JSON path')
|
|
211
|
+
.option('--draft <name>', 'Save a named Cloud draft without publishing Production')
|
|
211
212
|
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
212
213
|
.option('--replace-restored', 'Intentionally replace a restored live release')
|
|
213
214
|
.action(runCommand(async (bundle, options) => {
|
|
@@ -231,6 +232,7 @@ program
|
|
|
231
232
|
.option('--project <project>', 'Target project identifier (advanced override)')
|
|
232
233
|
.option('--environment <environment>', 'Target environment identifier (advanced override)')
|
|
233
234
|
.option('--no-source', 'Exclude project source files from the deployment bundle')
|
|
235
|
+
.option('--draft <name>', 'Save a named Cloud draft without publishing Production')
|
|
234
236
|
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
235
237
|
.option('--replace-restored', 'Intentionally replace a restored live release')
|
|
236
238
|
.option('--mcp-config', 'Also print MCP client configuration for the hosted endpoint')
|
|
@@ -7,12 +7,14 @@ export interface SubmitDeploymentOptions {
|
|
|
7
7
|
release?: string;
|
|
8
8
|
endpoint?: string;
|
|
9
9
|
replaceRestored?: boolean;
|
|
10
|
+
draft?: string;
|
|
10
11
|
/** Also print MCP client configuration for the hosted endpoint. */
|
|
11
12
|
mcpConfig?: boolean;
|
|
12
13
|
}
|
|
13
14
|
export interface DeployOptions {
|
|
14
15
|
endpoint?: string;
|
|
15
16
|
replaceRestored?: boolean;
|
|
17
|
+
draft?: string;
|
|
16
18
|
project?: string;
|
|
17
19
|
environment?: string;
|
|
18
20
|
bundleOutput?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AASA,OAAO,EACL,mCAAmC,EAEnC,KAAK,4BAA4B,EAElC,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AASA,OAAO,EACL,mCAAmC,EAEnC,KAAK,4BAA4B,EAElC,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EACL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EACL,oBAAoB,EACrB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,sBAAsB,EACtB,+BAA+B,EAChC,MAAM,iBAAiB,CAAC;AAMzB,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,mCAAmC,CAAC;IACtE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,mBAAmB,CAAC;IAChD,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,oBAAoB,CAAC;CACvD;AAED,MAAM,WAAW,kBAAmB,SAAQ,4BAA4B;IACtE,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,sBAAsB,CAAC;IACzD,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,+BAA+B,CAAC;IAC3E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,uBAAuB,CAAC;CAC5D;AA4DD,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,uBAA4B,EACrC,YAAY,GAAE,4BAAiC,GAC9C,OAAO,CAAC,4BAA4B,CAAC,CA8FvC;AAsBD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CA0CvC"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { prepareProtocolDeploymentReleaseEnvelope, } from '@hypequery/protocol';
|
|
4
4
|
import { DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, } from '../utils/deployment-bundle.js';
|
|
5
5
|
import { createHttpDeploymentUploadTransport, DeploymentUploadError, } from '../utils/deployment-upload.js';
|
|
6
|
+
import { validateDraftDeploymentOptions } from '../utils/draft-deployment.js';
|
|
6
7
|
import { logger } from '../utils/logger.js';
|
|
7
8
|
import { resolveDeploymentCredential } from '../utils/cloud-deployment-access.js';
|
|
8
9
|
import { fetchLiveDeployment } from '../utils/live-deployment.js';
|
|
@@ -73,6 +74,7 @@ export async function submitDeploymentCommand(bundlePath, options = {}, dependen
|
|
|
73
74
|
+ 'Usage: hypequery deployment:submit analytics/hypequery-deployment '
|
|
74
75
|
+ '--release analytics/hypequery-deployment.release.json');
|
|
75
76
|
}
|
|
77
|
+
validateDraftDeploymentOptions(options);
|
|
76
78
|
const releasePath = requiredConfiguration(options.release, 'Missing required --release <path>.');
|
|
77
79
|
const { endpoint: deploymentEndpoint, token } = await resolveDeploymentCredential(options.endpoint, dependencies);
|
|
78
80
|
let bundle;
|
|
@@ -95,7 +97,7 @@ export async function submitDeploymentCommand(bundlePath, options = {}, dependen
|
|
|
95
97
|
if (release.release.bundleIdentity !== bundle.identity) {
|
|
96
98
|
throw new DeploymentUploadError('HQ_UPLOAD_IDENTITY_MISMATCH', 'Release bundle identity does not match the verified deployment bundle.');
|
|
97
99
|
}
|
|
98
|
-
const live = await (dependencies.fetchLive ?? fetchLiveDeployment)({
|
|
100
|
+
const live = options.draft !== undefined ? null : await (dependencies.fetchLive ?? fetchLiveDeployment)({
|
|
99
101
|
endpoint: deploymentEndpoint,
|
|
100
102
|
token,
|
|
101
103
|
target: release.release.target,
|
|
@@ -115,8 +117,20 @@ export async function submitDeploymentCommand(bundlePath, options = {}, dependen
|
|
|
115
117
|
? { expectedActivationRevision: live.active?.revision ?? null }
|
|
116
118
|
: {}),
|
|
117
119
|
...(options.replaceRestored ? { replaceRestored: true } : {}),
|
|
120
|
+
...(options.draft !== undefined ? { draft: options.draft } : {}),
|
|
118
121
|
};
|
|
119
122
|
const result = await createTransport(transportOptions).submit(bundle, release);
|
|
123
|
+
if (options.draft !== undefined) {
|
|
124
|
+
if (!result.draft || result.draft.name !== options.draft) {
|
|
125
|
+
throw new DeploymentUploadError('HQ_UPLOAD_INVALID_RESPONSE', 'Deployment service did not acknowledge the requested draft.');
|
|
126
|
+
}
|
|
127
|
+
logger.success(`Draft saved: ${result.draft.name}`);
|
|
128
|
+
logger.info(`Draft ID: ${result.draft.draftId}`);
|
|
129
|
+
logger.info(`Staging endpoint: ${result.draft.stagingUrl}`);
|
|
130
|
+
logger.info(`Release identity: ${result.releaseIdentity}`);
|
|
131
|
+
logger.info('Review and publish this draft in Cloud when ready.');
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
120
134
|
logger.success(result.status === 'accepted'
|
|
121
135
|
? 'Deployment release accepted'
|
|
122
136
|
: 'Deployment release already exists');
|
|
@@ -150,6 +164,7 @@ async function rejectBundleDirectorySource(sourcePath) {
|
|
|
150
164
|
+ `${sourcePath} --release <file>\`.`);
|
|
151
165
|
}
|
|
152
166
|
export async function deployCommand(sourcePath, options = {}, dependencies = {}) {
|
|
167
|
+
validateDraftDeploymentOptions(options);
|
|
153
168
|
if (!sourcePath) {
|
|
154
169
|
throw new Error('Missing Cloud publication module path.\n\n'
|
|
155
170
|
+ 'Usage: hypequery deploy analytics/cloud.ts');
|
|
@@ -182,5 +197,6 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
|
|
|
182
197
|
endpoint: options.endpoint,
|
|
183
198
|
replaceRestored: options.replaceRestored,
|
|
184
199
|
mcpConfig: options.mcpConfig,
|
|
200
|
+
...(options.draft !== undefined ? { draft: options.draft } : {}),
|
|
185
201
|
}, dependencies);
|
|
186
202
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-client.d.ts","sourceRoot":"","sources":["../../src/commands/generate-client.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,qBAAqB;IAAG,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;AAE5F,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"generate-client.d.ts","sourceRoot":"","sources":["../../src/commands/generate-client.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,qBAAqB;IAAG,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;AAE5F,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAoChH"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { decodeProtocolClientContract } from '@hypequery/protocol';
|
|
3
3
|
import { renderClientContract } from '../generators/client-contract/render.js';
|
|
4
|
-
import { checkClientOutputPath, readClientArtifact, readClientOutput } from '../utils/client-contract-file.js';
|
|
4
|
+
import { checkClientOutputPath, describeClientContractError, readClientArtifact, readClientOutput } from '../utils/client-contract-file.js';
|
|
5
5
|
import { writeGeneratedFileAtomically } from '../utils/generated-file.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
7
|
import { updateCommandTelemetry, setCommandTelemetryError } from '../utils/telemetry/command-context.js';
|
|
@@ -16,7 +16,14 @@ export async function generateClientCommand(artifact, options = {}) {
|
|
|
16
16
|
throw new Error('Client input and output must be different files.');
|
|
17
17
|
await checkClientOutputPath(output);
|
|
18
18
|
const current = await readClientOutput(output);
|
|
19
|
-
const
|
|
19
|
+
const text = await readClientArtifact(input);
|
|
20
|
+
let contract;
|
|
21
|
+
try {
|
|
22
|
+
contract = decodeProtocolClientContract(text);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw describeClientContractError(error);
|
|
26
|
+
}
|
|
20
27
|
if (contract.view !== 'build')
|
|
21
28
|
throw new Error('Client generation requires a build view exported by the definition owner.');
|
|
22
29
|
const contents = renderClientContract(contract);
|
|
@@ -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;
|
|
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;AAS1E,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;IACtB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,OAAO,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;AAuTN;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB;;;;;;;;GAyEtE"}
|
|
@@ -7,45 +7,14 @@
|
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { getClickHouseClient } from '../utils/clickhouse-client.js';
|
|
10
|
+
import { clickHouseDimensionType, namedTupleElements, } from '../utils/clickhouse-dimension-type.js';
|
|
11
|
+
import { createFieldNamer } from '../utils/field-namer.js';
|
|
12
|
+
const SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10
13
|
/**
|
|
11
|
-
*
|
|
12
|
-
*/
|
|
13
|
-
function clickhouseToDimensionType(chType) {
|
|
14
|
-
const normalized = chType.toLowerCase();
|
|
15
|
-
// String types
|
|
16
|
-
if (normalized.includes('string') || normalized.includes('enum')) {
|
|
17
|
-
return 'string';
|
|
18
|
-
}
|
|
19
|
-
// Numeric types
|
|
20
|
-
if (normalized.includes('int') ||
|
|
21
|
-
normalized.includes('float') ||
|
|
22
|
-
normalized.includes('double') ||
|
|
23
|
-
normalized.includes('decimal')) {
|
|
24
|
-
return 'number';
|
|
25
|
-
}
|
|
26
|
-
// Date/Time types
|
|
27
|
-
if (normalized.includes('date') || normalized.includes('datetime')) {
|
|
28
|
-
return 'timestamp';
|
|
29
|
-
}
|
|
30
|
-
// Boolean
|
|
31
|
-
if (normalized.includes('bool')) {
|
|
32
|
-
return 'boolean';
|
|
33
|
-
}
|
|
34
|
-
// Default to string for unknown types
|
|
35
|
-
return 'string';
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Check if a column is likely a timestamp column
|
|
14
|
+
* Whether a column can be the dataset's timeKey: its values are dates or times.
|
|
39
15
|
*/
|
|
40
16
|
function isTimestampColumn(column) {
|
|
41
|
-
|
|
42
|
-
const type = column.type.toLowerCase();
|
|
43
|
-
return (type.includes('date') ||
|
|
44
|
-
type.includes('datetime') ||
|
|
45
|
-
name.includes('timestamp') ||
|
|
46
|
-
name.includes('_at') ||
|
|
47
|
-
name === 'created' ||
|
|
48
|
-
name === 'updated');
|
|
17
|
+
return clickHouseDimensionType(column.type) === 'timestamp';
|
|
49
18
|
}
|
|
50
19
|
/**
|
|
51
20
|
* Check if a column name is worth presenting as a possible tenant key.
|
|
@@ -65,11 +34,7 @@ function isTenantKeyCandidate(column) {
|
|
|
65
34
|
* Check if a column is numeric and suitable for measures
|
|
66
35
|
*/
|
|
67
36
|
function isNumericColumn(column) {
|
|
68
|
-
|
|
69
|
-
return (type.includes('int') ||
|
|
70
|
-
type.includes('float') ||
|
|
71
|
-
type.includes('double') ||
|
|
72
|
-
type.includes('decimal'));
|
|
37
|
+
return clickHouseDimensionType(column.type) === 'number';
|
|
73
38
|
}
|
|
74
39
|
/**
|
|
75
40
|
* Check if a numeric column is more likely an identifier than an additive value.
|
|
@@ -169,39 +134,93 @@ async function generateDatasetForTable(client, tableName, tenantColumn) {
|
|
|
169
134
|
'Review them and add tenantKey explicitly only after configuring trusted runtime tenant scope.',
|
|
170
135
|
});
|
|
171
136
|
}
|
|
172
|
-
// Generate dimensions
|
|
137
|
+
// Generate dimensions. A named tuple becomes one dimension per scalar
|
|
138
|
+
// element; columns no dimension type describes are left out with a note.
|
|
139
|
+
const fields = [];
|
|
140
|
+
const skippedColumns = [];
|
|
141
|
+
for (const column of columns) {
|
|
142
|
+
const dimensionType = clickHouseDimensionType(column.type);
|
|
143
|
+
if (dimensionType) {
|
|
144
|
+
fields.push({ column, type: dimensionType, name: columnToCamelCase(column.name) });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const elements = SQL_IDENTIFIER.test(column.name) ? namedTupleElements(column.type) : null;
|
|
148
|
+
if (elements?.length) {
|
|
149
|
+
for (const element of elements) {
|
|
150
|
+
fields.push({
|
|
151
|
+
column,
|
|
152
|
+
element: element.name,
|
|
153
|
+
type: element.type,
|
|
154
|
+
name: columnToCamelCase(`${column.name}_${element.name}`),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
skippedColumns.push(column);
|
|
160
|
+
}
|
|
161
|
+
// Field names are SQL aliases, so none may shadow a source column another
|
|
162
|
+
// field reads. A column spelled like its field keeps that name, then other
|
|
163
|
+
// columns, then tuple elements; a clash takes a numeric suffix instead.
|
|
164
|
+
const nameField = createFieldNamer(columns.map((column) => column.name));
|
|
165
|
+
const naming = [
|
|
166
|
+
...fields.filter((field) => !field.element && field.name === field.column.name),
|
|
167
|
+
...fields.filter((field) => !field.element && field.name !== field.column.name),
|
|
168
|
+
...fields.filter((field) => field.element),
|
|
169
|
+
];
|
|
170
|
+
for (const field of naming) {
|
|
171
|
+
const name = nameField(field.name, field.element ? undefined : field.column.name);
|
|
172
|
+
if (name !== field.name)
|
|
173
|
+
field.renamedFrom = field.name;
|
|
174
|
+
field.name = name;
|
|
175
|
+
}
|
|
176
|
+
const fieldNameByColumn = new Map(fields.filter((field) => !field.element).map((field) => [field.column.name, field.name]));
|
|
173
177
|
const dimensionLines = [];
|
|
174
178
|
for (const column of columns) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
dimensionLines.push(` ${fieldName}: dimension.${dimensionType}({ label: '${label}' }),`);
|
|
179
|
+
if (skippedColumns.includes(column)) {
|
|
180
|
+
// ClickHouse can spell a nested type over several lines; keep the note on one comment line.
|
|
181
|
+
const type = column.type.replace(/\s+/g, ' ');
|
|
182
|
+
dimensionLines.push(` // ${column.name} (${type}) is not included: no dimension type describes its values.`);
|
|
183
|
+
continue;
|
|
181
184
|
}
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
for (const field of fields.filter((candidate) => candidate.column === column)) {
|
|
186
|
+
const label = generateLabel(column.name);
|
|
187
|
+
if (field.renamedFrom) {
|
|
188
|
+
dimensionLines.push(` // ${field.element ? `${column.name}.${field.element}` : column.name} is ${field.name}: ${field.renamedFrom} is taken by another field or column.`);
|
|
189
|
+
}
|
|
190
|
+
if (field.element) {
|
|
191
|
+
dimensionLines.push(` ${field.name}: dimension.${field.type}({ sql: "tupleElement(${column.name}, '${field.element}')", ` +
|
|
192
|
+
`dependencies: ['${column.name}'], label: '${label} ${generateLabel(field.element)}' }),`);
|
|
193
|
+
}
|
|
194
|
+
else if (column.name === field.name) {
|
|
195
|
+
// No column option needed when the field is spelled like the column
|
|
196
|
+
dimensionLines.push(` ${field.name}: dimension.${field.type}({ label: '${label}' }),`);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
dimensionLines.push(` ${field.name}: dimension.${field.type}({ column: '${column.name}', label: '${label}' }),`);
|
|
200
|
+
}
|
|
184
201
|
}
|
|
185
202
|
}
|
|
186
203
|
// Generate measures for numeric value columns only. IDs and coordinates stay
|
|
187
204
|
// dimensions because summing or averaging them is usually noise.
|
|
188
205
|
const numericColumns = columns.filter(isMeasureCandidate);
|
|
189
206
|
const measureLines = [];
|
|
207
|
+
const countField = fields[0]?.name;
|
|
190
208
|
if (numericColumns.length > 0) {
|
|
191
209
|
measureLines.push(` // Count measures`);
|
|
192
|
-
measureLines.push(` totalCount: measure.count('${
|
|
210
|
+
measureLines.push(` ${nameField('totalCount')}: measure.count('${countField}', { label: 'Total Count' }),`);
|
|
193
211
|
measureLines.push(``);
|
|
194
212
|
for (const column of numericColumns) {
|
|
195
|
-
const fieldName =
|
|
213
|
+
const fieldName = fieldNameByColumn.get(column.name);
|
|
214
|
+
const measureSuffix = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
|
|
196
215
|
const label = generateLabel(column.name);
|
|
197
216
|
measureLines.push(` // ${label} measures`);
|
|
198
|
-
measureLines.push(` total${
|
|
199
|
-
measureLines.push(` avg${
|
|
217
|
+
measureLines.push(` ${nameField(`total${measureSuffix}`)}: measure.sum('${fieldName}', { label: 'Total ${label}' }),`);
|
|
218
|
+
measureLines.push(` ${nameField(`avg${measureSuffix}`)}: measure.avg('${fieldName}', { label: 'Average ${label}' }),`);
|
|
200
219
|
measureLines.push(``);
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
|
-
else {
|
|
204
|
-
measureLines.push(` totalCount: measure.count('${
|
|
222
|
+
else if (countField) {
|
|
223
|
+
measureLines.push(` ${nameField('totalCount')}: measure.count('${countField}', { label: 'Total Count' }),`);
|
|
205
224
|
}
|
|
206
225
|
// Build the dataset definition
|
|
207
226
|
const datasetName = tableToPascalCase(tableName);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** A dataset dimension type the scaffolder can emit. */
|
|
2
|
+
export type ScaffoldDimensionType = 'string' | 'number' | 'boolean' | 'timestamp';
|
|
3
|
+
export interface NamedTupleElement {
|
|
4
|
+
name: string;
|
|
5
|
+
type: ScaffoldDimensionType;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The value type behind `Nullable`, `LowCardinality` and
|
|
9
|
+
* `SimpleAggregateFunction(fn, T)` wrappers; none changes what a value is.
|
|
10
|
+
*/
|
|
11
|
+
export declare function unwrapClickHouseType(type: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Maps a ClickHouse column type to a dataset dimension type, or null when no
|
|
14
|
+
* scalar dimension type describes its values (Array, Tuple, Map, geo, JSON,
|
|
15
|
+
* Interval, ...). The outer type name is matched exactly: a substring match
|
|
16
|
+
* reads `Point` or `Array(Int32)` as numbers. Mirrors Cloud's `protocolTypeOf`
|
|
17
|
+
* so a table scaffolds with the same field types in both.
|
|
18
|
+
*/
|
|
19
|
+
export declare function clickHouseDimensionType(type: string): ScaffoldDimensionType | null;
|
|
20
|
+
/**
|
|
21
|
+
* The scalar elements of a named `Tuple(name T, ...)` column, each of which can
|
|
22
|
+
* be its own dimension through `tupleElement`. Null for any other type,
|
|
23
|
+
* including unnamed tuples, whose positions carry no meaning to name a field
|
|
24
|
+
* after. Elements without a scalar type or a plain identifier name are left out.
|
|
25
|
+
*/
|
|
26
|
+
export declare function namedTupleElements(type: string): NamedTupleElement[] | null;
|
|
27
|
+
//# sourceMappingURL=clickhouse-dimension-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clickhouse-dimension-type.d.ts","sourceRoot":"","sources":["../../src/utils/clickhouse-dimension-type.ts"],"names":[],"mappings":"AAEA,wDAAwD;AACxD,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;AAElF,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,qBAAqB,CAAC;CAC7B;AAID;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASzD;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAOlF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,EAAE,GAAG,IAAI,CAgB3E"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { matchTypeCall } from './clickhouse-type-utils.js';
|
|
2
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3
|
+
/**
|
|
4
|
+
* The value type behind `Nullable`, `LowCardinality` and
|
|
5
|
+
* `SimpleAggregateFunction(fn, T)` wrappers; none changes what a value is.
|
|
6
|
+
*/
|
|
7
|
+
export function unwrapClickHouseType(type) {
|
|
8
|
+
let current = type.trim();
|
|
9
|
+
const simple = /^SimpleAggregateFunction\(\s*[A-Za-z_][A-Za-z0-9_]*\s*,\s*(.+)\)$/s.exec(current);
|
|
10
|
+
if (simple)
|
|
11
|
+
current = simple[1].trim();
|
|
12
|
+
for (;;) {
|
|
13
|
+
const wrapped = /^(?:Nullable|LowCardinality)\((.*)\)$/s.exec(current);
|
|
14
|
+
if (!wrapped)
|
|
15
|
+
return current;
|
|
16
|
+
current = wrapped[1].trim();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Maps a ClickHouse column type to a dataset dimension type, or null when no
|
|
21
|
+
* scalar dimension type describes its values (Array, Tuple, Map, geo, JSON,
|
|
22
|
+
* Interval, ...). The outer type name is matched exactly: a substring match
|
|
23
|
+
* reads `Point` or `Array(Int32)` as numbers. Mirrors Cloud's `protocolTypeOf`
|
|
24
|
+
* so a table scaffolds with the same field types in both.
|
|
25
|
+
*/
|
|
26
|
+
export function clickHouseDimensionType(type) {
|
|
27
|
+
const value = unwrapClickHouseType(type);
|
|
28
|
+
if (/^(?:U?Int\d+|Float\d+|Decimal\d*)(?:\(.*\))?$/s.test(value))
|
|
29
|
+
return 'number';
|
|
30
|
+
if (/^(?:Date|Date32|DateTime|DateTime64)(?:\(.*\))?$/s.test(value))
|
|
31
|
+
return 'timestamp';
|
|
32
|
+
if (value === 'Bool')
|
|
33
|
+
return 'boolean';
|
|
34
|
+
if (/^(?:String|FixedString|UUID|Enum\d*|IPv4|IPv6)(?:\(.*\))?$/s.test(value))
|
|
35
|
+
return 'string';
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The scalar elements of a named `Tuple(name T, ...)` column, each of which can
|
|
40
|
+
* be its own dimension through `tupleElement`. Null for any other type,
|
|
41
|
+
* including unnamed tuples, whose positions carry no meaning to name a field
|
|
42
|
+
* after. Elements without a scalar type or a plain identifier name are left out.
|
|
43
|
+
*/
|
|
44
|
+
export function namedTupleElements(type) {
|
|
45
|
+
const parts = matchTypeCall(unwrapClickHouseType(type), 'Tuple');
|
|
46
|
+
if (!parts?.length)
|
|
47
|
+
return null;
|
|
48
|
+
const elements = [];
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
const named = /^(\S+)\s+(.+)$/s.exec(part);
|
|
51
|
+
// Element types never contain top-level whitespace before their first
|
|
52
|
+
// `(`, so a part without a name-then-space prefix is an unnamed element.
|
|
53
|
+
if (!named || named[1].includes('('))
|
|
54
|
+
return null;
|
|
55
|
+
const elementType = clickHouseDimensionType(named[2]);
|
|
56
|
+
if (IDENTIFIER.test(named[1]) && elementType) {
|
|
57
|
+
elements.push({ name: named[1], type: elementType });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return elements;
|
|
61
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clickhouse-type-utils.d.ts","sourceRoot":"","sources":["../../src/utils/clickhouse-type-utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"clickhouse-type-utils.d.ts","sourceRoot":"","sources":["../../src/utils/clickhouse-type-utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAqDzD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG3E;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAGzE;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,UASpD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,WAE7C"}
|
|
@@ -3,10 +3,18 @@ export function splitTopLevelArgs(value) {
|
|
|
3
3
|
let current = '';
|
|
4
4
|
let depth = 0;
|
|
5
5
|
let quote = null;
|
|
6
|
+
let escaped = false;
|
|
6
7
|
for (const char of value) {
|
|
7
8
|
if (quote) {
|
|
8
9
|
current += char;
|
|
9
|
-
|
|
10
|
+
// ClickHouse escapes a quote inside a literal with a backslash (`'can\'t'`).
|
|
11
|
+
if (escaped) {
|
|
12
|
+
escaped = false;
|
|
13
|
+
}
|
|
14
|
+
else if (char === '\\') {
|
|
15
|
+
escaped = true;
|
|
16
|
+
}
|
|
17
|
+
else if (char === quote) {
|
|
10
18
|
quote = null;
|
|
11
19
|
}
|
|
12
20
|
continue;
|
|
@@ -3,4 +3,9 @@ export declare function checkClientOutputPath(filePath: string): Promise<void>;
|
|
|
3
3
|
/** Bound allocation before reading; O_NOFOLLOW also refuses a swapped final symlink. */
|
|
4
4
|
export declare function readClientArtifact(filePath: string, maxBytes?: number): Promise<string>;
|
|
5
5
|
export declare function readClientOutput(filePath: string): Promise<string | undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* Say where a client contract is invalid. An unknown field usually means the
|
|
8
|
+
* contract was exported by a newer hypequery than this CLI understands.
|
|
9
|
+
*/
|
|
10
|
+
export declare function describeClientContractError(error: unknown): unknown;
|
|
6
11
|
//# sourceMappingURL=client-contract-file.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-contract-file.d.ts","sourceRoot":"","sources":["../../src/utils/client-contract-file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client-contract-file.d.ts","sourceRoot":"","sources":["../../src/utils/client-contract-file.ts"],"names":[],"mappings":"AAMA,qFAAqF;AACrF,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3E;AAsBD,wFAAwF;AACxF,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBhG;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAMpF;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQnE"}
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
|
-
import { lstat, open } from 'node:fs/promises';
|
|
2
|
+
import { lstat, open, realpath, stat } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { ProtocolClientContractError } from '@hypequery/protocol';
|
|
5
|
+
import { pointer } from './json-pointer.js';
|
|
4
6
|
/** Refuse filesystem indirection for generated output and its existing ancestors. */
|
|
5
7
|
export async function checkClientOutputPath(filePath) {
|
|
6
|
-
|
|
8
|
+
const target = path.resolve(filePath);
|
|
9
|
+
let current = target;
|
|
7
10
|
while (true) {
|
|
8
11
|
try {
|
|
9
12
|
const info = await lstat(current);
|
|
10
|
-
if (info.isSymbolicLink()
|
|
11
|
-
throw
|
|
13
|
+
if (info.isSymbolicLink())
|
|
14
|
+
throw await throughLink(filePath, target, current);
|
|
15
|
+
if (current === target ? !info.isFile() : !info.isDirectory()) {
|
|
16
|
+
throw new Error(current === target
|
|
17
|
+
? `Client output ${filePath} must be a regular file.`
|
|
18
|
+
: `Client output ${filePath} must be a regular file under ordinary directories: ${current} is not a directory.`);
|
|
12
19
|
}
|
|
13
20
|
}
|
|
14
21
|
catch (error) {
|
|
15
|
-
|
|
22
|
+
// Missing, or below a file: the ancestor that exists is checked next.
|
|
23
|
+
const code = error.code;
|
|
24
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR')
|
|
16
25
|
throw error;
|
|
17
26
|
}
|
|
18
27
|
const parent = path.dirname(current);
|
|
@@ -21,6 +30,27 @@ export async function checkClientOutputPath(filePath) {
|
|
|
21
30
|
current = parent;
|
|
22
31
|
}
|
|
23
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Name the link, and the same output without it, e.g. /tmp → /private/tmp on macOS.
|
|
35
|
+
* The path is suggested only when the link resolves to what that position needs:
|
|
36
|
+
* a directory above the output, a regular file at the output itself.
|
|
37
|
+
*/
|
|
38
|
+
async function throughLink(filePath, target, link) {
|
|
39
|
+
const message = link === target
|
|
40
|
+
? `Client output ${filePath} must be a regular file, not a symbolic link.`
|
|
41
|
+
: `Client output ${filePath} must be a regular file under ordinary directories: ${link} is a symbolic link.`;
|
|
42
|
+
try {
|
|
43
|
+
const real = await realpath(link);
|
|
44
|
+
const info = await stat(real);
|
|
45
|
+
if (link === target ? !info.isFile() : !info.isDirectory())
|
|
46
|
+
return new Error(message);
|
|
47
|
+
const resolved = path.join(real, path.relative(link, target));
|
|
48
|
+
return new Error(`${message} Use the path it points to instead: ${resolved}`);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return new Error(message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
24
54
|
/** Bound allocation before reading; O_NOFOLLOW also refuses a swapped final symlink. */
|
|
25
55
|
export async function readClientArtifact(filePath, maxBytes = 1_048_576) {
|
|
26
56
|
const handle = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
@@ -56,3 +86,17 @@ export async function readClientOutput(filePath) {
|
|
|
56
86
|
throw error;
|
|
57
87
|
}
|
|
58
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Say where a client contract is invalid. An unknown field usually means the
|
|
91
|
+
* contract was exported by a newer hypequery than this CLI understands.
|
|
92
|
+
*/
|
|
93
|
+
export function describeClientContractError(error) {
|
|
94
|
+
if (!(error instanceof ProtocolClientContractError))
|
|
95
|
+
return error;
|
|
96
|
+
const at = error.field === undefined ? error.path : pointer(error.path, error.field);
|
|
97
|
+
const where = at ? ` at ${at}` : '';
|
|
98
|
+
const hint = error.code === 'HQ_CLIENT_UNKNOWN_FIELD'
|
|
99
|
+
? ' The contract may come from a newer hypequery than this CLI; upgrade @hypequery/cli.'
|
|
100
|
+
: '';
|
|
101
|
+
return new Error(`${error.message.replace(/\.$/, '')}${where} (${error.code}).${hint}`, { cause: error });
|
|
102
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ReadableStream } from 'node:stream/web';
|
|
2
2
|
import { type PreparedProtocolDeploymentReleaseEnvelope } from '@hypequery/protocol';
|
|
3
3
|
import { type VerifiedDeploymentBundle } from './deployment-bundle.js';
|
|
4
|
-
import type { DeploymentSubmissionResponse } from '@hypequery/deployment';
|
|
5
|
-
|
|
4
|
+
import type { DeploymentSubmissionResponse as BaseDeploymentSubmissionResponse } from '@hypequery/deployment';
|
|
5
|
+
import { type DraftDeploymentResult } from './draft-deployment.js';
|
|
6
|
+
export type DeploymentSubmissionResponse = BaseDeploymentSubmissionResponse & {
|
|
7
|
+
readonly draft?: DraftDeploymentResult;
|
|
8
|
+
};
|
|
6
9
|
export type DeploymentUploadErrorCode = 'HQ_UPLOAD_CONFIGURATION' | 'HQ_UPLOAD_IDENTITY_MISMATCH' | 'HQ_UPLOAD_BUNDLE_CHANGED' | 'HQ_UPLOAD_NETWORK' | 'HQ_UPLOAD_REJECTED' | 'HQ_UPLOAD_INVALID_RESPONSE';
|
|
7
10
|
export declare class DeploymentUploadError extends Error {
|
|
8
11
|
readonly code: DeploymentUploadErrorCode;
|
|
@@ -29,6 +32,7 @@ export interface HttpDeploymentUploadTransportOptions {
|
|
|
29
32
|
readonly token: string;
|
|
30
33
|
readonly expectedActivationRevision?: string | null;
|
|
31
34
|
readonly replaceRestored?: boolean;
|
|
35
|
+
readonly draft?: string;
|
|
32
36
|
readonly timeoutMs?: number;
|
|
33
37
|
readonly fetch?: DeploymentFetch;
|
|
34
38
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment-upload.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-upload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAIL,KAAK,yCAAyC,EAC/C,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"deployment-upload.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-upload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAIL,KAAK,yCAAyC,EAC/C,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,4BAA4B,IAAI,gCAAgC,EAAE,MAAM,uBAAuB,CAAC;AAC9G,OAAO,EAA8D,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAE/H,MAAM,MAAM,4BAA4B,GAAG,gCAAgC,GAAG;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,qBAAqB,CAAC;CACxC,CAAC;AAQF,MAAM,MAAM,yBAAyB,GACjC,yBAAyB,GACzB,6BAA6B,GAC7B,0BAA0B,GAC1B,mBAAmB,GACnB,oBAAoB,GACpB,4BAA4B,CAAC;AAEjC,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAM9E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED,MAAM,MAAM,eAAe,GAAG,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,mBAAmB,KACtB,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAErC,MAAM,WAAW,oCAAoC;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,0BAA0B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpD,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CACJ,MAAM,EAAE,wBAAwB,EAChC,OAAO,EAAE,yCAAyC,GACjD,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAC1C;AAoXD,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,oCAAoC,GAC5C,yBAAyB,CAqG3B"}
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { prepareProtocolDeploymentBundleManifest, prepareProtocolDeploymentReleaseEnvelope, } from '@hypequery/protocol';
|
|
5
5
|
import { DEPLOYMENT_BUNDLE_MANIFEST, } from './deployment-bundle.js';
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
|
+
import { parseDraftDeploymentResult, validateDraftDeploymentOptions } from './draft-deployment.js';
|
|
7
8
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
8
9
|
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
9
10
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
@@ -249,15 +250,18 @@ function exactRecord(value, fields) {
|
|
|
249
250
|
}
|
|
250
251
|
return record;
|
|
251
252
|
}
|
|
252
|
-
function validateSubmissionResponse(input, bundleIdentity, releaseIdentity, status) {
|
|
253
|
+
function validateSubmissionResponse(input, bundleIdentity, releaseIdentity, status, expectedDraft) {
|
|
253
254
|
const value = exactRecord(input, [
|
|
254
255
|
'kind',
|
|
255
256
|
'version',
|
|
256
257
|
'status',
|
|
257
258
|
'releaseIdentity',
|
|
258
259
|
'bundleIdentity',
|
|
260
|
+
...(expectedDraft ? ['draft'] : []),
|
|
259
261
|
]);
|
|
262
|
+
const draft = expectedDraft && value ? parseDraftDeploymentResult(value.draft, expectedDraft) : undefined;
|
|
260
263
|
if (!value
|
|
264
|
+
|| (expectedDraft && !draft)
|
|
261
265
|
|| value.kind !== 'hypequery-deployment-submission'
|
|
262
266
|
|| value.version !== 1
|
|
263
267
|
|| (value.status !== 'accepted' && value.status !== 'already-exists')
|
|
@@ -275,6 +279,7 @@ function validateSubmissionResponse(input, bundleIdentity, releaseIdentity, stat
|
|
|
275
279
|
status: value.status,
|
|
276
280
|
releaseIdentity: value.releaseIdentity,
|
|
277
281
|
bundleIdentity: value.bundleIdentity,
|
|
282
|
+
...(draft ? { draft } : {}),
|
|
278
283
|
});
|
|
279
284
|
}
|
|
280
285
|
function rejectedMessage(response, body) {
|
|
@@ -319,6 +324,10 @@ function nestedUploadError(input) {
|
|
|
319
324
|
}
|
|
320
325
|
const defaultFetch = async (input, init) => fetch(input, init);
|
|
321
326
|
export function createHttpDeploymentUploadTransport(options) {
|
|
327
|
+
validateDraftDeploymentOptions(options);
|
|
328
|
+
if (options.draft !== undefined && options.expectedActivationRevision !== undefined) {
|
|
329
|
+
configurationError('Draft uploads cannot specify an expected Production activation revision.');
|
|
330
|
+
}
|
|
322
331
|
const endpoint = endpointUrl(options.endpoint);
|
|
323
332
|
const token = bearerToken(options.token);
|
|
324
333
|
const timeoutMs = timeout(options.timeoutMs);
|
|
@@ -347,6 +356,7 @@ export function createHttpDeploymentUploadTransport(options) {
|
|
|
347
356
|
'Idempotency-Key': preparedRelease.identity,
|
|
348
357
|
'X-HypeQuery-Bundle-Identity': manifest.identity,
|
|
349
358
|
'X-HypeQuery-Release-Identity': preparedRelease.identity,
|
|
359
|
+
...(options.draft !== undefined ? { 'X-HypeQuery-Draft-Name': options.draft } : {}),
|
|
350
360
|
...(options.expectedActivationRevision !== undefined
|
|
351
361
|
? {
|
|
352
362
|
'X-HypeQuery-Expected-Activation-Revision': options.expectedActivationRevision ?? 'none',
|
|
@@ -387,7 +397,7 @@ export function createHttpDeploymentUploadTransport(options) {
|
|
|
387
397
|
catch {
|
|
388
398
|
throw new DeploymentUploadError('HQ_UPLOAD_INVALID_RESPONSE', 'Deployment service returned invalid JSON.', response.status);
|
|
389
399
|
}
|
|
390
|
-
return validateSubmissionResponse(input, manifest.identity, preparedRelease.identity, response.status);
|
|
400
|
+
return validateSubmissionResponse(input, manifest.identity, preparedRelease.identity, response.status, options.draft !== undefined ? { name: options.draft, endpoint, project: preparedRelease.release.target.project } : undefined);
|
|
391
401
|
},
|
|
392
402
|
};
|
|
393
403
|
return Object.freeze(transport);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface DraftDeploymentResult {
|
|
2
|
+
readonly draftId: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly stagingUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function validateDraftDeploymentOptions(options: {
|
|
7
|
+
draft?: string;
|
|
8
|
+
replaceRestored?: boolean;
|
|
9
|
+
}): void;
|
|
10
|
+
/** Accept only the requested draft and a staging endpoint on the deployment service. */
|
|
11
|
+
export declare function parseDraftDeploymentResult(input: unknown, expected: {
|
|
12
|
+
name: string;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
project: string;
|
|
15
|
+
}): DraftDeploymentResult | undefined;
|
|
16
|
+
//# sourceMappingURL=draft-deployment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft-deployment.d.ts","sourceRoot":"","sources":["../../src/utils/draft-deployment.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,IAAI,CAQP;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC5D,qBAAqB,GAAG,SAAS,CAmBnC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function validateDraftDeploymentOptions(options) {
|
|
2
|
+
if (options.draft === undefined)
|
|
3
|
+
return;
|
|
4
|
+
if (options.draft.length > 64 || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(options.draft)) {
|
|
5
|
+
throw new Error('--draft must be a lowercase name with letters, numbers and single hyphens (max 64 characters).');
|
|
6
|
+
}
|
|
7
|
+
if (options.replaceRestored) {
|
|
8
|
+
throw new Error('--draft cannot be combined with --replace-restored; draft uploads do not replace Production.');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Accept only the requested draft and a staging endpoint on the deployment service. */
|
|
12
|
+
export function parseDraftDeploymentResult(input, expected) {
|
|
13
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)
|
|
14
|
+
|| Object.getPrototypeOf(input) !== Object.prototype)
|
|
15
|
+
return undefined;
|
|
16
|
+
const value = input;
|
|
17
|
+
if (Object.keys(value).sort().join(',') !== 'draftId,name,stagingUrl'
|
|
18
|
+
|| typeof value.draftId !== 'string'
|
|
19
|
+
|| !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value.draftId)
|
|
20
|
+
|| value.name !== expected.name
|
|
21
|
+
|| typeof value.stagingUrl !== 'string')
|
|
22
|
+
return undefined;
|
|
23
|
+
let url;
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(value.stagingUrl, expected.endpoint);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (url.origin !== new URL(expected.endpoint).origin || url.username || url.password
|
|
31
|
+
|| url.search || url.hash
|
|
32
|
+
|| url.pathname !== `/api/gateway/${encodeURIComponent(expected.project)}/draft/${value.draftId}`)
|
|
33
|
+
return undefined;
|
|
34
|
+
return Object.freeze({ draftId: value.draftId, name: expected.name, stagingUrl: url.href });
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hands out the field names of one generated dataset. A field's name is also
|
|
3
|
+
* its SQL alias, and ClickHouse resolves an alias before a column of the same
|
|
4
|
+
* name, so a field may take a source column's name only when it reads exactly
|
|
5
|
+
* that column; otherwise every expression reading that column would read the
|
|
6
|
+
* aliased field instead. A name that is taken, or is another column's name,
|
|
7
|
+
* gets the lowest free numeric suffix (`fareTotal2`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function createFieldNamer(columnNames: readonly string[]): (base: string, ownColumn?: string) => string;
|
|
10
|
+
//# sourceMappingURL=field-namer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-namer.d.ts","sourceRoot":"","sources":["../../src/utils/field-namer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,IAGrD,MAAM,MAAM,EAAE,YAAY,MAAM,KAAG,MAAM,CAQlD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hands out the field names of one generated dataset. A field's name is also
|
|
3
|
+
* its SQL alias, and ClickHouse resolves an alias before a column of the same
|
|
4
|
+
* name, so a field may take a source column's name only when it reads exactly
|
|
5
|
+
* that column; otherwise every expression reading that column would read the
|
|
6
|
+
* aliased field instead. A name that is taken, or is another column's name,
|
|
7
|
+
* gets the lowest free numeric suffix (`fareTotal2`).
|
|
8
|
+
*/
|
|
9
|
+
export function createFieldNamer(columnNames) {
|
|
10
|
+
const columns = new Set(columnNames);
|
|
11
|
+
const taken = new Set();
|
|
12
|
+
return (base, ownColumn) => {
|
|
13
|
+
let name = base;
|
|
14
|
+
for (let suffix = 2; taken.has(name) || (columns.has(name) && name !== ownColumn); suffix += 1) {
|
|
15
|
+
name = `${base}${suffix}`;
|
|
16
|
+
}
|
|
17
|
+
taken.add(name);
|
|
18
|
+
return name;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-pointer.d.ts","sourceRoot":"","sources":["../../src/utils/json-pointer.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD"}
|
|
@@ -625,7 +625,7 @@ export declare const EVENT_CATALOG: {
|
|
|
625
625
|
readonly flags_used: {
|
|
626
626
|
readonly kind: "enum_list";
|
|
627
627
|
readonly description: string;
|
|
628
|
-
readonly values: readonly ("--path" | "--style" | "--database" | "--chdb-path" | "--auth" | "--all-tables" | "--tables" | "--exclude-tables" | "--no-example" | "--no-interactive" | "--force" | "--skip-connection" | "--port" | "--hostname" | "--no-watch" | "--no-cache" | "--cache" | "--redis-url" | "--open" | "--cors" | "--quiet" | "--tenant" | "--self-test" | "--url" | "--client-contract-url" | "--output" | "--tenant-column" | "--check" | "--diff" | "--cloud-url" | "--environment" | "--bundle-output" | "--release-output" | "--project" | "--no-source" | "--endpoint" | "--replace-restored" | "--mcp-config" | "--release" | "--no-telemetry" | "--help" | "--version")[];
|
|
628
|
+
readonly values: readonly ("--path" | "--style" | "--database" | "--chdb-path" | "--auth" | "--all-tables" | "--tables" | "--exclude-tables" | "--no-example" | "--no-interactive" | "--force" | "--skip-connection" | "--port" | "--hostname" | "--no-watch" | "--no-cache" | "--cache" | "--redis-url" | "--open" | "--cors" | "--quiet" | "--tenant" | "--self-test" | "--url" | "--client-contract-url" | "--output" | "--tenant-column" | "--check" | "--diff" | "--cloud-url" | "--environment" | "--bundle-output" | "--release-output" | "--project" | "--no-source" | "--endpoint" | "--replace-restored" | "--mcp-config" | "--draft" | "--release" | "--no-telemetry" | "--help" | "--version")[];
|
|
629
629
|
};
|
|
630
630
|
readonly outcome: {
|
|
631
631
|
readonly kind: "enum";
|
|
@@ -18,11 +18,11 @@ export declare const COMMAND_FLAGS: {
|
|
|
18
18
|
readonly 'generate:client': readonly ["--output", "--check", "--force"];
|
|
19
19
|
readonly login: readonly ["--cloud-url", "--environment"];
|
|
20
20
|
readonly logout: readonly [];
|
|
21
|
-
readonly deploy: readonly ["--bundle-output", "--release-output", "--project", "--environment", "--no-source", "--endpoint", "--replace-restored", "--mcp-config"];
|
|
21
|
+
readonly deploy: readonly ["--bundle-output", "--release-output", "--project", "--environment", "--no-source", "--endpoint", "--replace-restored", "--mcp-config", "--draft"];
|
|
22
22
|
readonly 'deployment:build': readonly ["--bundle-output", "--no-source"];
|
|
23
23
|
readonly 'deployment:validate': readonly [];
|
|
24
24
|
readonly 'deployment:release': readonly ["--project", "--environment", "--output"];
|
|
25
|
-
readonly 'deployment:submit': readonly ["--release", "--endpoint", "--replace-restored"];
|
|
25
|
+
readonly 'deployment:submit': readonly ["--release", "--endpoint", "--replace-restored", "--draft"];
|
|
26
26
|
readonly 'deployment:status': readonly ["--project", "--environment", "--endpoint", "--mcp-config"];
|
|
27
27
|
readonly pull: readonly ["--output", "--project", "--environment", "--endpoint"];
|
|
28
28
|
readonly diff: readonly ["--project", "--environment", "--endpoint"];
|
|
@@ -44,11 +44,11 @@ export declare const VALUE_FLAGS: {
|
|
|
44
44
|
readonly 'generate:client': readonly ["--output"];
|
|
45
45
|
readonly login: readonly ["--cloud-url", "--environment"];
|
|
46
46
|
readonly logout: readonly [];
|
|
47
|
-
readonly deploy: readonly ["--bundle-output", "--release-output", "--project", "--environment", "--endpoint"];
|
|
47
|
+
readonly deploy: readonly ["--bundle-output", "--release-output", "--project", "--environment", "--endpoint", "--draft"];
|
|
48
48
|
readonly 'deployment:build': readonly ["--bundle-output"];
|
|
49
49
|
readonly 'deployment:validate': readonly [];
|
|
50
50
|
readonly 'deployment:release': readonly ["--project", "--environment", "--output"];
|
|
51
|
-
readonly 'deployment:submit': readonly ["--release", "--endpoint"];
|
|
51
|
+
readonly 'deployment:submit': readonly ["--release", "--endpoint", "--draft"];
|
|
52
52
|
readonly 'deployment:status': readonly ["--project", "--environment", "--endpoint"];
|
|
53
53
|
readonly pull: readonly ["--output", "--project", "--environment", "--endpoint"];
|
|
54
54
|
readonly diff: readonly ["--project", "--environment", "--endpoint"];
|
|
@@ -27,11 +27,11 @@ export const COMMAND_FLAGS = {
|
|
|
27
27
|
'generate:client': ['--output', '--check', '--force'],
|
|
28
28
|
login: ['--cloud-url', '--environment'],
|
|
29
29
|
logout: [],
|
|
30
|
-
deploy: ['--bundle-output', '--release-output', '--project', '--environment', '--no-source', '--endpoint', '--replace-restored', '--mcp-config'],
|
|
30
|
+
deploy: ['--bundle-output', '--release-output', '--project', '--environment', '--no-source', '--endpoint', '--replace-restored', '--mcp-config', '--draft'],
|
|
31
31
|
'deployment:build': ['--bundle-output', '--no-source'],
|
|
32
32
|
'deployment:validate': [],
|
|
33
33
|
'deployment:release': ['--project', '--environment', '--output'],
|
|
34
|
-
'deployment:submit': ['--release', '--endpoint', '--replace-restored'],
|
|
34
|
+
'deployment:submit': ['--release', '--endpoint', '--replace-restored', '--draft'],
|
|
35
35
|
'deployment:status': ['--project', '--environment', '--endpoint', '--mcp-config'],
|
|
36
36
|
pull: ['--output', '--project', '--environment', '--endpoint'],
|
|
37
37
|
diff: ['--project', '--environment', '--endpoint'],
|
|
@@ -50,11 +50,11 @@ export const VALUE_FLAGS = {
|
|
|
50
50
|
'generate:client': ['--output'],
|
|
51
51
|
login: ['--cloud-url', '--environment'],
|
|
52
52
|
logout: [],
|
|
53
|
-
deploy: ['--bundle-output', '--release-output', '--project', '--environment', '--endpoint'],
|
|
53
|
+
deploy: ['--bundle-output', '--release-output', '--project', '--environment', '--endpoint', '--draft'],
|
|
54
54
|
'deployment:build': ['--bundle-output'],
|
|
55
55
|
'deployment:validate': [],
|
|
56
56
|
'deployment:release': ['--project', '--environment', '--output'],
|
|
57
|
-
'deployment:submit': ['--release', '--endpoint'],
|
|
57
|
+
'deployment:submit': ['--release', '--endpoint', '--draft'],
|
|
58
58
|
'deployment:status': ['--project', '--environment', '--endpoint'],
|
|
59
59
|
pull: ['--output', '--project', '--environment', '--endpoint'],
|
|
60
60
|
diff: ['--project', '--environment', '--endpoint'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "CLI for scaffolding type-safe ClickHouse analytics, semantic datasets, React manifests, and deployments",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"clickhouse",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"open": "^11.0.4",
|
|
31
31
|
"ora": "^8.0.1",
|
|
32
32
|
"prompts": "^2.4.2",
|
|
33
|
-
"@hypequery/deployment": "0.11.
|
|
34
|
-
"@hypequery/protocol": "0.24.
|
|
33
|
+
"@hypequery/deployment": "0.11.1",
|
|
34
|
+
"@hypequery/protocol": "0.24.1"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
37
|
"@napi-rs/keyring": "2.1.2"
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"@vitest/coverage-v8": "^5.0.5",
|
|
55
55
|
"typescript": "^6.0.3",
|
|
56
56
|
"vitest": "^5.0.5",
|
|
57
|
-
"@hypequery/mcp": "0.11.
|
|
58
|
-
"@hypequery/serve": "0.24.
|
|
57
|
+
"@hypequery/mcp": "0.11.1",
|
|
58
|
+
"@hypequery/serve": "0.24.1"
|
|
59
59
|
},
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|