@hypequery/cli 1.27.0 → 1.28.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 +14 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -2
- package/dist/commands/deploy.d.ts +4 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +31 -1
- package/dist/commands/deployment.d.ts.map +1 -1
- package/dist/commands/deployment.js +11 -0
- package/dist/commands/generate-client.d.ts +13 -1
- package/dist/commands/generate-client.d.ts.map +1 -1
- package/dist/commands/generate-client.js +33 -6
- package/dist/generators/client-contract/render.d.ts +7 -1
- package/dist/generators/client-contract/render.d.ts.map +1 -1
- package/dist/generators/client-contract/render.js +5 -3
- package/dist/generators/dataset-generator.d.ts.map +1 -1
- package/dist/generators/dataset-generator.js +2 -12
- package/dist/templates/cloud.d.ts.map +1 -1
- package/dist/templates/cloud.js +3 -0
- package/dist/utils/cloud-client-contract.d.ts +20 -0
- package/dist/utils/cloud-client-contract.d.ts.map +1 -0
- package/dist/utils/cloud-client-contract.js +52 -0
- package/dist/utils/live-deployment.d.ts +1 -1
- package/dist/utils/live-deployment.d.ts.map +1 -1
- package/dist/utils/read-capped.d.ts +6 -0
- package/dist/utils/read-capped.d.ts.map +1 -0
- package/dist/utils/read-capped.js +36 -0
- package/dist/utils/shell-quote.d.ts +6 -0
- package/dist/utils/shell-quote.d.ts.map +1 -0
- package/dist/utils/shell-quote.js +7 -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/dist/utils/tenant-key-candidates.d.ts +19 -0
- package/dist/utils/tenant-key-candidates.d.ts.map +1 -0
- package/dist/utils/tenant-key-candidates.js +63 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -37,13 +37,26 @@ npx hypequery init \
|
|
|
37
37
|
| `hypequery dev` | Local API, docs, and hot reload |
|
|
38
38
|
| `hypequery generate` | Fresh TypeScript types from ClickHouse or chDB |
|
|
39
39
|
| `hypequery generate:datasets` | Dataset definitions scaffolded from tables |
|
|
40
|
-
| `hypequery generate:client` | TypeScript API types and routes from a Python client contract |
|
|
40
|
+
| `hypequery generate:client` | TypeScript API types and routes from a Python client contract or the live Cloud release (`--cloud`) |
|
|
41
41
|
| `hypequery generate:manifest` | A browser-safe route manifest for React hooks |
|
|
42
42
|
| `hypequery pull` / `diff` | Live source inspection and comparison |
|
|
43
43
|
| `hypequery deployment:status` | Live release and Cloud-provided connection URLs |
|
|
44
44
|
|
|
45
45
|
Non-interactive ClickHouse commands read `CLICKHOUSE_URL`, `CLICKHOUSE_DATABASE`, `CLICKHOUSE_USERNAME`, and `CLICKHOUSE_PASSWORD`.
|
|
46
46
|
|
|
47
|
+
## Generate a client for Cloud datasets
|
|
48
|
+
|
|
49
|
+
After `hypequery login`, generate typed React routes and types from the live
|
|
50
|
+
Cloud release, or have every publish regenerate them:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx hypequery generate:client --cloud --output src/hypequery-client.ts
|
|
54
|
+
npx hypequery deploy analytics/cloud.ts --client src/hypequery-client.ts
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The file also exports `clientBaseUrl`. See
|
|
58
|
+
[Connect React to hypequery Cloud](https://hypequery.com/docs/react/cloud).
|
|
59
|
+
|
|
47
60
|
## Generate a client for Python endpoints
|
|
48
61
|
|
|
49
62
|
Export a build contract from your Python definition-only factory, then generate
|
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;AAgTD,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -166,11 +166,15 @@ program
|
|
|
166
166
|
await generateDatasetsCommand(options);
|
|
167
167
|
}));
|
|
168
168
|
program
|
|
169
|
-
.command('generate:client
|
|
170
|
-
.description('Generate browser-safe TypeScript API types and routes from a client contract JSON file')
|
|
169
|
+
.command('generate:client [contract]')
|
|
170
|
+
.description('Generate browser-safe TypeScript API types and routes from a client contract JSON file or the live Cloud release')
|
|
171
171
|
.option('-o, --output <path>', 'Output TypeScript file (default: analytics/hypequery-client.ts)')
|
|
172
172
|
.option('--check', 'Check for drift without writing')
|
|
173
173
|
.option('--force', 'Replace an existing output file')
|
|
174
|
+
.option('--cloud', 'Use the live Cloud release of the target from `hypequery login`')
|
|
175
|
+
.option('--project <project>', 'With --cloud: target project identifier (advanced override)')
|
|
176
|
+
.option('--environment <environment>', 'With --cloud: target environment identifier (advanced override)')
|
|
177
|
+
.option('--endpoint <url>', 'With --cloud: HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
174
178
|
.action(runCommand(async (contract, options) => {
|
|
175
179
|
await generateClientCommand(contract, options);
|
|
176
180
|
}));
|
|
@@ -236,6 +240,7 @@ program
|
|
|
236
240
|
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
237
241
|
.option('--replace-restored', 'Intentionally replace a restored live release')
|
|
238
242
|
.option('--mcp-config', 'Also print MCP client configuration for the hosted endpoint')
|
|
243
|
+
.option('--client <path>', 'Regenerate this typed client file from the new live release')
|
|
239
244
|
.action(runCommand(async (source, options) => {
|
|
240
245
|
await deployCommand(source, options);
|
|
241
246
|
}));
|
|
@@ -3,6 +3,7 @@ import { type StoredCloudCredential } from '../utils/cloud-credential-store.js';
|
|
|
3
3
|
import { fetchLiveDeployment } from '../utils/live-deployment.js';
|
|
4
4
|
import { fetchHostedEndpoints } from '../utils/hosted-endpoints.js';
|
|
5
5
|
import { buildDeploymentCommand, prepareDeploymentReleaseCommand } from './deployment.js';
|
|
6
|
+
import { generateClientCommand } from './generate-client.js';
|
|
6
7
|
export interface SubmitDeploymentOptions {
|
|
7
8
|
release?: string;
|
|
8
9
|
endpoint?: string;
|
|
@@ -21,6 +22,8 @@ export interface DeployOptions {
|
|
|
21
22
|
releaseOutput?: string;
|
|
22
23
|
source?: boolean;
|
|
23
24
|
mcpConfig?: boolean;
|
|
25
|
+
/** Regenerate this typed client file from the new live release. */
|
|
26
|
+
client?: string;
|
|
24
27
|
}
|
|
25
28
|
export interface SubmitDeploymentDependencies {
|
|
26
29
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
@@ -33,6 +36,7 @@ export interface DeployDependencies extends SubmitDeploymentDependencies {
|
|
|
33
36
|
readonly buildDeployment?: typeof buildDeploymentCommand;
|
|
34
37
|
readonly prepareDeploymentRelease?: typeof prepareDeploymentReleaseCommand;
|
|
35
38
|
readonly submitDeployment?: typeof submitDeploymentCommand;
|
|
39
|
+
readonly generateClient?: typeof generateClientCommand;
|
|
36
40
|
}
|
|
37
41
|
export declare function submitDeploymentCommand(bundlePath: string | undefined, options?: SubmitDeploymentOptions, dependencies?: SubmitDeploymentDependencies): Promise<DeploymentSubmissionResponse>;
|
|
38
42
|
export declare function deployCommand(sourcePath: string | undefined, options?: DeployOptions, dependencies?: DeployDependencies): Promise<DeploymentSubmissionResponse>;
|
|
@@ -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;AAIvC,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;AACzB,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAM7D,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;IACpB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;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;IAC3D,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,qBAAqB,CAAC;CACxD;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,CAyEvC"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -4,12 +4,14 @@ 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
6
|
import { validateDraftDeploymentOptions } from '../utils/draft-deployment.js';
|
|
7
|
+
import { shellQuote } from '../utils/shell-quote.js';
|
|
7
8
|
import { logger } from '../utils/logger.js';
|
|
8
9
|
import { resolveDeploymentCredential } from '../utils/cloud-deployment-access.js';
|
|
9
10
|
import { fetchLiveDeployment } from '../utils/live-deployment.js';
|
|
10
11
|
import { fetchHostedEndpoints, } from '../utils/hosted-endpoints.js';
|
|
11
12
|
import { printHostedEndpoints } from '../utils/hosted-endpoints-output.js';
|
|
12
13
|
import { buildDeploymentCommand, prepareDeploymentReleaseCommand, } from './deployment.js';
|
|
14
|
+
import { generateClientCommand } from './generate-client.js';
|
|
13
15
|
const MAX_RELEASE_FILE_BYTES = 16 * 1024;
|
|
14
16
|
const DEFAULT_BUNDLE_PATH = 'analytics/hypequery-deployment';
|
|
15
17
|
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
@@ -192,11 +194,39 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
|
|
|
192
194
|
loadCredential: dependencies.loadCredential,
|
|
193
195
|
outputFlagLabel: '--release-output',
|
|
194
196
|
});
|
|
195
|
-
|
|
197
|
+
const result = await submit(bundlePath, {
|
|
196
198
|
release: releasePath,
|
|
197
199
|
endpoint: options.endpoint,
|
|
198
200
|
replaceRestored: options.replaceRestored,
|
|
199
201
|
mcpConfig: options.mcpConfig,
|
|
200
202
|
...(options.draft !== undefined ? { draft: options.draft } : {}),
|
|
201
203
|
}, dependencies);
|
|
204
|
+
// A draft isn't live, so the client keeps matching Production.
|
|
205
|
+
if (options.client !== undefined && options.draft !== undefined) {
|
|
206
|
+
logger.info(`${options.client} was not regenerated: drafts are not live, so the client keeps matching the live release.`);
|
|
207
|
+
}
|
|
208
|
+
if (options.client !== undefined && options.draft === undefined) {
|
|
209
|
+
try {
|
|
210
|
+
await (dependencies.generateClient ?? generateClientCommand)(undefined, {
|
|
211
|
+
cloud: true,
|
|
212
|
+
output: options.client,
|
|
213
|
+
force: true,
|
|
214
|
+
endpoint: options.endpoint,
|
|
215
|
+
project: options.project,
|
|
216
|
+
environment: options.environment,
|
|
217
|
+
}, dependencies);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
// The release is live either way; say how to catch the client up.
|
|
221
|
+
logger.warn(`Deployed, but ${options.client} was not regenerated: `
|
|
222
|
+
+ (error instanceof Error ? error.message : String(error)));
|
|
223
|
+
const flags = [
|
|
224
|
+
['--project', options.project],
|
|
225
|
+
['--environment', options.environment],
|
|
226
|
+
['--endpoint', options.endpoint],
|
|
227
|
+
].flatMap(([flag, value]) => value === undefined ? [] : [` ${flag} ${shellQuote(value)}`]).join('');
|
|
228
|
+
logger.indent(`Run \`hypequery generate:client --cloud --output ${shellQuote(options.client)} --force${flags}\` to retry.`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
202
232
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACvC,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACvC,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAG5C,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oCAAoC;IACnD,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IAC7D;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAgBD,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAmCrC;AAUD,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CAqErC;AAiED,wBAAsB,+BAA+B,CACnD,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,+BAAoC,EAC7C,YAAY,GAAE,oCAAyC,GACtD,OAAO,CAAC,iCAAiC,CAAC,CAkE5C"}
|
|
@@ -6,7 +6,17 @@ import { captureDeploymentSourceSnapshot } from '../utils/deployment-source-snap
|
|
|
6
6
|
import { loadCloudPublication } from '../utils/load-cloud-publication.js';
|
|
7
7
|
import { logger } from '../utils/logger.js';
|
|
8
8
|
import { loadCloudCredential, } from '../utils/cloud-credential-store.js';
|
|
9
|
+
import { unscopedTenantCandidates } from '../utils/tenant-key-candidates.js';
|
|
9
10
|
const DEFAULT_BUNDLE_OUTPUT = 'analytics/hypequery-deployment';
|
|
11
|
+
/** Name datasets that would show every customer's rows; Cloud asks the same in Customer access. */
|
|
12
|
+
function warnUnscopedTenantCandidates(contract) {
|
|
13
|
+
for (const { dataset, columns } of unscopedTenantCandidates(contract)) {
|
|
14
|
+
logger.warn(`Dataset "${dataset}" has a customer column (${columns.join(', ')}) but no tenantKey, `
|
|
15
|
+
+ 'so every caller can read every customer\'s rows.');
|
|
16
|
+
logger.indent(`If each customer should see only their own rows, set tenantKey: '${columns[0]}' on the dataset and deploy again.`);
|
|
17
|
+
logger.indent('If every customer may read all of it, mark it shared when Cloud asks during onboarding, or under Customer access.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
export async function buildDeploymentCommand(sourcePath, options = {}) {
|
|
11
21
|
if (!sourcePath) {
|
|
12
22
|
throw new Error('Missing Cloud publication module path.\n\n'
|
|
@@ -33,6 +43,7 @@ export async function buildDeploymentCommand(sourcePath, options = {}) {
|
|
|
33
43
|
+ `${prepared.contract.datasets.length === 1 ? 'dataset' : 'datasets'}, 0 runtime artifacts`);
|
|
34
44
|
logger.info(`Bundle identity: ${bundle.identity}`);
|
|
35
45
|
logger.info(`Deployment identity: ${prepared.identity}`);
|
|
46
|
+
warnUnscopedTenantCandidates(prepared.contract);
|
|
36
47
|
return prepared.contract;
|
|
37
48
|
}
|
|
38
49
|
function describeContract(contract) {
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
import { type CloudDeploymentAccessDependencies } from '../utils/cloud-deployment-access.js';
|
|
2
|
+
import { fetchHostedEndpoints } from '../utils/hosted-endpoints.js';
|
|
3
|
+
import type { LiveDeploymentFetch } from '../utils/live-deployment.js';
|
|
1
4
|
export interface GenerateClientOptions {
|
|
2
5
|
output?: string;
|
|
3
6
|
check?: boolean;
|
|
4
7
|
force?: boolean;
|
|
8
|
+
/** Read the live Cloud release's contract instead of a file (target and credential from `hypequery login`). */
|
|
9
|
+
cloud?: boolean;
|
|
10
|
+
project?: string;
|
|
11
|
+
environment?: string;
|
|
12
|
+
endpoint?: string;
|
|
5
13
|
}
|
|
6
|
-
export
|
|
14
|
+
export interface GenerateClientDependencies extends CloudDeploymentAccessDependencies {
|
|
15
|
+
readonly fetch?: LiveDeploymentFetch;
|
|
16
|
+
readonly fetchEndpoints?: typeof fetchHostedEndpoints;
|
|
17
|
+
}
|
|
18
|
+
export declare function generateClientCommand(artifact: string | undefined, options?: GenerateClientOptions, dependencies?: GenerateClientDependencies): Promise<void>;
|
|
7
19
|
//# sourceMappingURL=generate-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-client.d.ts","sourceRoot":"","sources":["../../src/commands/generate-client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generate-client.d.ts","sourceRoot":"","sources":["../../src/commands/generate-client.ts"],"names":[],"mappings":"AAKA,OAAO,EAA+B,KAAK,iCAAiC,EAAE,MAAM,qCAAqC,CAAC;AAG1H,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAKvE,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+GAA+G;IAC/G,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA2B,SAAQ,iCAAiC;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,oBAAoB,CAAC;CACvD;AAcD,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,GAAE,qBAA0B,EACnC,YAAY,GAAE,0BAA+B,GAC5C,OAAO,CAAC,IAAI,CAAC,CAgDf"}
|
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { decodeProtocolClientContract } from '@hypequery/protocol';
|
|
3
|
-
import { renderClientContract } from '../generators/client-contract/render.js';
|
|
3
|
+
import { GENERATED_CLIENT_HEADER, renderClientContract } from '../generators/client-contract/render.js';
|
|
4
4
|
import { checkClientOutputPath, describeClientContractError, readClientArtifact, readClientOutput } from '../utils/client-contract-file.js';
|
|
5
|
+
import { clientOrigin, fetchCloudClientContract } from '../utils/cloud-client-contract.js';
|
|
6
|
+
import { resolveDeploymentCredential } from '../utils/cloud-deployment-access.js';
|
|
7
|
+
import { deploymentTargetFromOptions } from '../utils/deployment-target.js';
|
|
5
8
|
import { writeGeneratedFileAtomically } from '../utils/generated-file.js';
|
|
9
|
+
import { fetchHostedEndpoints } from '../utils/hosted-endpoints.js';
|
|
6
10
|
import { logger } from '../utils/logger.js';
|
|
7
11
|
import { updateCommandTelemetry, setCommandTelemetryError } from '../utils/telemetry/command-context.js';
|
|
8
12
|
import { countBucket } from '../utils/telemetry/buckets.js';
|
|
9
|
-
|
|
13
|
+
/** The live release's contract, and the origin it is served from when Cloud says. */
|
|
14
|
+
async function readCloudClientContract(options, dependencies) {
|
|
15
|
+
const access = await resolveDeploymentCredential(options.endpoint, dependencies);
|
|
16
|
+
const target = deploymentTargetFromOptions(options, access.storedCredential);
|
|
17
|
+
const request = { endpoint: access.endpoint, token: access.token, target, fetch: dependencies.fetch };
|
|
18
|
+
const [text, endpoints] = await Promise.all([
|
|
19
|
+
fetchCloudClientContract(request),
|
|
20
|
+
(dependencies.fetchEndpoints ?? fetchHostedEndpoints)(request),
|
|
21
|
+
]);
|
|
22
|
+
return { text, baseUrl: clientOrigin(endpoints?.rest.baseUrl) };
|
|
23
|
+
}
|
|
24
|
+
export async function generateClientCommand(artifact, options = {}, dependencies = {}) {
|
|
10
25
|
updateCommandTelemetry('generate:client', { mode: options.check ? 'check' : 'write', force: !!options.force, custom_output: options.output !== undefined });
|
|
11
26
|
if (options.force && options.check)
|
|
12
27
|
throw new Error('--force cannot be combined with --check.');
|
|
13
|
-
|
|
28
|
+
if (options.cloud ? artifact !== undefined : artifact === undefined) {
|
|
29
|
+
throw new Error(options.cloud
|
|
30
|
+
? 'Pass a client contract file or --cloud, not both.'
|
|
31
|
+
: 'Missing client contract. Pass a contract JSON file, or --cloud for the live Cloud release.');
|
|
32
|
+
}
|
|
33
|
+
if (!options.cloud && (options.project ?? options.environment ?? options.endpoint) !== undefined) {
|
|
34
|
+
throw new Error('--project, --environment and --endpoint apply only with --cloud.');
|
|
35
|
+
}
|
|
14
36
|
const output = path.resolve(options.output ?? 'analytics/hypequery-client.ts');
|
|
15
|
-
if (
|
|
37
|
+
if (artifact !== undefined && path.resolve(artifact) === output)
|
|
16
38
|
throw new Error('Client input and output must be different files.');
|
|
17
39
|
await checkClientOutputPath(output);
|
|
18
40
|
const current = await readClientOutput(output);
|
|
19
|
-
const text =
|
|
41
|
+
const { text, baseUrl } = artifact === undefined
|
|
42
|
+
? await readCloudClientContract(options, dependencies)
|
|
43
|
+
: { text: await readClientArtifact(path.resolve(artifact)), baseUrl: undefined };
|
|
20
44
|
let contract;
|
|
21
45
|
try {
|
|
22
46
|
contract = decodeProtocolClientContract(text);
|
|
@@ -26,7 +50,7 @@ export async function generateClientCommand(artifact, options = {}) {
|
|
|
26
50
|
}
|
|
27
51
|
if (contract.view !== 'build')
|
|
28
52
|
throw new Error('Client generation requires a build view exported by the definition owner.');
|
|
29
|
-
const contents = renderClientContract(contract);
|
|
53
|
+
const contents = renderClientContract(contract, { baseUrl });
|
|
30
54
|
if (Buffer.byteLength(contents) > 16 * 1_048_576)
|
|
31
55
|
throw new Error('Generated client exceeds the 16 MiB output limit.');
|
|
32
56
|
updateCommandTelemetry('generate:client', { endpoint_count_bucket: countBucket(contract.endpoints.length) });
|
|
@@ -44,6 +68,9 @@ export async function generateClientCommand(artifact, options = {}) {
|
|
|
44
68
|
}
|
|
45
69
|
if (current !== undefined && !options.force)
|
|
46
70
|
throw new Error('Refusing to overwrite client types; use --check or --force.');
|
|
71
|
+
if (current !== undefined && !current.startsWith(GENERATED_CLIENT_HEADER)) {
|
|
72
|
+
throw new Error(`Refusing to overwrite ${output}: it was not generated by hypequery generate:client.`);
|
|
73
|
+
}
|
|
47
74
|
await checkClientOutputPath(output);
|
|
48
75
|
if (await readClientOutput(output) !== current)
|
|
49
76
|
throw new Error('Client output changed during generation; retry after reviewing it.');
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export interface RenderClientContractOptions {
|
|
2
|
+
/** Where the endpoints are served; emitted as `clientBaseUrl` (its origin, since paths are absolute). */
|
|
3
|
+
readonly baseUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
/** First line of every generated client; overwrites are refused for files without it. */
|
|
6
|
+
export declare const GENERATED_CLIENT_HEADER = "// Generated by hypequery generate:client.";
|
|
1
7
|
/** Data-only generator: no API module evaluation, credentials, drivers or server dependencies. */
|
|
2
|
-
export declare function renderClientContract(source: unknown): string;
|
|
8
|
+
export declare function renderClientContract(source: unknown, options?: RenderClientContractOptions): string;
|
|
3
9
|
//# sourceMappingURL=render.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/generators/client-contract/render.ts"],"names":[],"mappings":"AAmCA,kGAAkG;AAClG,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/generators/client-contract/render.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,2BAA2B;IAC1C,yGAAyG;IACzG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,yFAAyF;AACzF,eAAO,MAAM,uBAAuB,+CAA+C,CAAC;AAEpF,kGAAkG;AAClG,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,GAAE,2BAAgC,GAAG,MAAM,CAqBvG"}
|
|
@@ -29,12 +29,14 @@ function endpointDoc(endpoint) {
|
|
|
29
29
|
const metric = endpoint.kind === 'metric' ? endpoint.annotations?.fields?.[Object.keys(endpoint.measures)[0]] : undefined;
|
|
30
30
|
return metric ?? endpoint.annotations;
|
|
31
31
|
}
|
|
32
|
+
/** First line of every generated client; overwrites are refused for files without it. */
|
|
33
|
+
export const GENERATED_CLIENT_HEADER = '// Generated by hypequery generate:client.';
|
|
32
34
|
/** Data-only generator: no API module evaluation, credentials, drivers or server dependencies. */
|
|
33
|
-
export function renderClientContract(source) {
|
|
35
|
+
export function renderClientContract(source, options = {}) {
|
|
34
36
|
const contract = validateProtocolClientContract(source);
|
|
35
37
|
const endpoints = [...contract.endpoints].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
36
38
|
const entries = endpoints.map(endpoint => ({ endpoint, key: endpoint.kind === 'dataset' ? `dataset:${endpoint.id}` : endpoint.id }));
|
|
37
|
-
return
|
|
39
|
+
return `${GENERATED_CLIENT_HEADER} Do not edit.
|
|
38
40
|
// Runtime validation still enforces selection dependencies, timezones and query budgets.
|
|
39
41
|
export interface ClientResponseMeta {
|
|
40
42
|
requestId: string;
|
|
@@ -50,5 +52,5 @@ export const clientManifest = {
|
|
|
50
52
|
${entries.map(({ endpoint, key }) => ` [${literal(key)}]: { method: 'POST', path: ${literal(endpoint.path)}, schemaHash: ${literal(endpoint.schemaHash)} },`).join('\n')}
|
|
51
53
|
} as const;
|
|
52
54
|
export const clientContractHash = ${literal(contract.contentHash)} as const;
|
|
53
|
-
`;
|
|
55
|
+
${options.baseUrl === undefined ? '' : `export const clientBaseUrl = ${literal(new URL(options.baseUrl).origin)} as const;\n`}`;
|
|
54
56
|
}
|
|
@@ -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;AAU1E,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;AAySN;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB;;;;;;;;GAyEtE"}
|
|
@@ -9,6 +9,7 @@ import path from 'path';
|
|
|
9
9
|
import { getClickHouseClient } from '../utils/clickhouse-client.js';
|
|
10
10
|
import { clickHouseDimensionType, namedTupleElements, } from '../utils/clickhouse-dimension-type.js';
|
|
11
11
|
import { createFieldNamer } from '../utils/field-namer.js';
|
|
12
|
+
import { isTenantKeyCandidateName } from '../utils/tenant-key-candidates.js';
|
|
12
13
|
const SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
13
14
|
/**
|
|
14
15
|
* Whether a column can be the dataset's timeKey: its values are dates or times.
|
|
@@ -16,19 +17,8 @@ const SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
16
17
|
function isTimestampColumn(column) {
|
|
17
18
|
return clickHouseDimensionType(column.type) === 'timestamp';
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Check if a column name is worth presenting as a possible tenant key.
|
|
21
|
-
*
|
|
22
|
-
* A naming match is never enough to activate tenant enforcement. `tenantKey`
|
|
23
|
-
* is a security boundary that must be chosen explicitly by the application.
|
|
24
|
-
*/
|
|
25
20
|
function isTenantKeyCandidate(column) {
|
|
26
|
-
|
|
27
|
-
return (name === 'tenant_id' ||
|
|
28
|
-
name === 'organization_id' ||
|
|
29
|
-
name === 'org_id' ||
|
|
30
|
-
name === 'account_id' ||
|
|
31
|
-
name === 'customer_id');
|
|
21
|
+
return isTenantKeyCandidateName(column.name);
|
|
32
22
|
}
|
|
33
23
|
/**
|
|
34
24
|
* Check if a column is numeric and suitable for measures
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/templates/cloud.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,wBAAgB,qBAAqB,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/templates/cloud.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C"}
|
package/dist/templates/cloud.js
CHANGED
|
@@ -5,6 +5,9 @@ import { datasets } from './datasets.js';
|
|
|
5
5
|
|
|
6
6
|
export const cloud = publishToCloud({
|
|
7
7
|
datasets,
|
|
8
|
+
// Empty roles and scopes still require authentication, but let any
|
|
9
|
+
// authenticated caller read every dataset. A dataset without tenantKey shows
|
|
10
|
+
// every customer's rows; review both in Cloud under Customer access.
|
|
8
11
|
access: { roles: [], scopes: [] },
|
|
9
12
|
});
|
|
10
13
|
`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
|
|
2
|
+
import { type LiveDeploymentFetch } from './live-deployment.js';
|
|
3
|
+
/**
|
|
4
|
+
* The origin a generated client calls. Refused when Cloud did not say, when it
|
|
5
|
+
* is not https (loopback aside), or when it carries a path: the client resolves
|
|
6
|
+
* its absolute endpoint paths against the origin, so a prefix would be dropped.
|
|
7
|
+
*/
|
|
8
|
+
export declare function clientOrigin(baseUrl: string | undefined): string;
|
|
9
|
+
/**
|
|
10
|
+
* The client contract of the target's live release, as Cloud serves it beside
|
|
11
|
+
* the deployment endpoint. Returned as text: the caller decodes and verifies it
|
|
12
|
+
* exactly as it would a contract file.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fetchCloudClientContract(input: {
|
|
15
|
+
readonly endpoint: string;
|
|
16
|
+
readonly token: string;
|
|
17
|
+
readonly target: ProtocolDeploymentReleaseTarget;
|
|
18
|
+
readonly fetch?: LiveDeploymentFetch;
|
|
19
|
+
}): Promise<string>;
|
|
20
|
+
//# sourceMappingURL=cloud-client-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-client-contract.d.ts","sourceRoot":"","sources":["../../src/utils/cloud-client-contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAW,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAOzE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAehE;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAAC,KAAK,EAAE;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;CACtC,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBlB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { liveUrl } from './live-deployment.js';
|
|
2
|
+
import { readCapped } from './read-capped.js';
|
|
3
|
+
const MAX_CONTRACT_BYTES = 1_048_576;
|
|
4
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
5
|
+
/**
|
|
6
|
+
* The origin a generated client calls. Refused when Cloud did not say, when it
|
|
7
|
+
* is not https (loopback aside), or when it carries a path: the client resolves
|
|
8
|
+
* its absolute endpoint paths against the origin, so a prefix would be dropped.
|
|
9
|
+
*/
|
|
10
|
+
export function clientOrigin(baseUrl) {
|
|
11
|
+
if (baseUrl === undefined) {
|
|
12
|
+
throw new Error('Cloud did not say where this release is served, so the client would have no base URL. Retry, or update Cloud.');
|
|
13
|
+
}
|
|
14
|
+
const url = new URL(baseUrl);
|
|
15
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && LOOPBACK_HOSTS.has(url.hostname))) {
|
|
16
|
+
throw new Error(`Cloud serves this release from ${url.origin}, which is not https.`);
|
|
17
|
+
}
|
|
18
|
+
if (url.pathname !== '/' || url.search || url.hash) {
|
|
19
|
+
throw new Error(`Cloud serves this release under ${baseUrl} because an endpoint path collides with a reserved Cloud path (such as /mcp). `
|
|
20
|
+
+ 'Rename that endpoint path and deploy again; generated clients call the customer origin directly.');
|
|
21
|
+
}
|
|
22
|
+
return url.origin;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The client contract of the target's live release, as Cloud serves it beside
|
|
26
|
+
* the deployment endpoint. Returned as text: the caller decodes and verifies it
|
|
27
|
+
* exactly as it would a contract file.
|
|
28
|
+
*/
|
|
29
|
+
export async function fetchCloudClientContract(input) {
|
|
30
|
+
const url = liveUrl(input.endpoint, input.target, 'client-contract');
|
|
31
|
+
if (!url) {
|
|
32
|
+
throw new Error('The deployment endpoint is not a Cloud submissions URL, so it serves no client contract.');
|
|
33
|
+
}
|
|
34
|
+
const response = await (input.fetch ?? fetch)(url, {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
headers: { Accept: 'application/json', Authorization: `Bearer ${input.token}` },
|
|
37
|
+
redirect: 'error',
|
|
38
|
+
signal: AbortSignal.timeout(30_000),
|
|
39
|
+
});
|
|
40
|
+
const target = `${input.target.project} / ${input.target.environment}`;
|
|
41
|
+
if (response.status === 401 || response.status === 403) {
|
|
42
|
+
throw new Error(`Cloud refused the client contract for ${target}. Run \`hypequery login\` again.`);
|
|
43
|
+
}
|
|
44
|
+
if (response.status === 404) {
|
|
45
|
+
throw new Error(`Cloud has no client contract for ${target}: publish a release first, `
|
|
46
|
+
+ 'or this Cloud version does not serve client contracts yet.');
|
|
47
|
+
}
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Cloud could not return the client contract for ${target} (HTTP ${response.status}).`);
|
|
50
|
+
}
|
|
51
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(await readCapped(response, MAX_CONTRACT_BYTES));
|
|
52
|
+
}
|
|
@@ -31,7 +31,7 @@ export type LiveDeploymentFetch = typeof fetch;
|
|
|
31
31
|
* the endpoint is not a Cloud submissions URL. Refuses anything but https (or
|
|
32
32
|
* loopback http) so a credential is never sent in the clear.
|
|
33
33
|
*/
|
|
34
|
-
export declare function liveUrl(endpoint: string, target: ProtocolDeploymentReleaseTarget, resource: 'state' | 'source' | 'endpoints'): string | undefined;
|
|
34
|
+
export declare function liveUrl(endpoint: string, target: ProtocolDeploymentReleaseTarget, resource: 'state' | 'source' | 'endpoints' | 'client-contract'): string | undefined;
|
|
35
35
|
export declare function fetchLiveDeployment(input: {
|
|
36
36
|
readonly endpoint: string;
|
|
37
37
|
readonly token: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-deployment.d.ts","sourceRoot":"","sources":["../../src/utils/live-deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAO7B,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;KACzB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;QACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC3B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;QAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAC;KACxC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,OAAO,KAAK,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,+BAA+B,EACvC,QAAQ,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"live-deployment.d.ts","sourceRoot":"","sources":["../../src/utils/live-deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAO7B,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;KACzB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;QACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC3B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;QAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAC;KACxC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,OAAO,KAAK,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,+BAA+B,EACvC,QAAQ,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,iBAAiB,GAC7D,MAAM,GAAG,SAAS,CAkBpB;AAsJD,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;CACtC,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAuCtC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A response body, refused once it passes `maxBytes`: by the declared length
|
|
3
|
+
* before reading, and while streaming when no length is declared.
|
|
4
|
+
*/
|
|
5
|
+
export declare function readCapped(response: Response, maxBytes: number): Promise<Uint8Array>;
|
|
6
|
+
//# sourceMappingURL=read-capped.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-capped.d.ts","sourceRoot":"","sources":["../../src/utils/read-capped.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA6B1F"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A response body, refused once it passes `maxBytes`: by the declared length
|
|
3
|
+
* before reading, and while streaming when no length is declared.
|
|
4
|
+
*/
|
|
5
|
+
export async function readCapped(response, maxBytes) {
|
|
6
|
+
const tooLarge = () => new Error(`The response exceeds ${maxBytes} bytes.`);
|
|
7
|
+
const declared = Number(response.headers.get('content-length'));
|
|
8
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
9
|
+
// Release the connection rather than leave the body unread.
|
|
10
|
+
await response.body?.cancel();
|
|
11
|
+
throw tooLarge();
|
|
12
|
+
}
|
|
13
|
+
if (!response.body)
|
|
14
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
15
|
+
const reader = response.body.getReader();
|
|
16
|
+
const chunks = [];
|
|
17
|
+
let total = 0;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const { done, value } = await reader.read();
|
|
20
|
+
if (done)
|
|
21
|
+
break;
|
|
22
|
+
total += value.byteLength;
|
|
23
|
+
if (total > maxBytes) {
|
|
24
|
+
await reader.cancel();
|
|
25
|
+
throw tooLarge();
|
|
26
|
+
}
|
|
27
|
+
chunks.push(value);
|
|
28
|
+
}
|
|
29
|
+
const bytes = new Uint8Array(total);
|
|
30
|
+
let offset = 0;
|
|
31
|
+
for (const chunk of chunks) {
|
|
32
|
+
bytes.set(chunk, offset);
|
|
33
|
+
offset += chunk.byteLength;
|
|
34
|
+
}
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-quote.d.ts","sourceRoot":"","sources":["../../src/utils/shell-quote.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One argument as a POSIX shell word, for commands printed for the user to
|
|
3
|
+
* copy. Plain words stay as they are; anything else is single-quoted.
|
|
4
|
+
*/
|
|
5
|
+
export function shellQuote(value) {
|
|
6
|
+
return /^[A-Za-z0-9_./:=@%+-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
7
|
+
}
|
|
@@ -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
|
|
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" | "--project" | "--environment" | "--endpoint" | "--cloud-url" | "--bundle-output" | "--release-output" | "--no-source" | "--replace-restored" | "--mcp-config" | "--draft" | "--client" | "--release" | "--no-telemetry" | "--help" | "--version")[];
|
|
629
629
|
};
|
|
630
630
|
readonly outcome: {
|
|
631
631
|
readonly kind: "enum";
|
|
@@ -15,10 +15,10 @@ export declare const COMMAND_FLAGS: {
|
|
|
15
15
|
readonly 'generate:types': readonly ["--output", "--path", "--tables", "--database", "--chdb-path"];
|
|
16
16
|
readonly 'generate:datasets': readonly ["--output", "--path", "--tables", "--exclude-tables", "--tenant-column", "--force", "--check", "--diff"];
|
|
17
17
|
readonly 'generate:manifest': readonly ["--output"];
|
|
18
|
-
readonly 'generate:client': readonly ["--output", "--check", "--force"];
|
|
18
|
+
readonly 'generate:client': readonly ["--output", "--check", "--force", "--cloud", "--project", "--environment", "--endpoint"];
|
|
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", "--draft"];
|
|
21
|
+
readonly deploy: readonly ["--bundle-output", "--release-output", "--project", "--environment", "--no-source", "--endpoint", "--replace-restored", "--mcp-config", "--draft", "--client"];
|
|
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"];
|
|
@@ -41,10 +41,10 @@ export declare const VALUE_FLAGS: {
|
|
|
41
41
|
readonly 'generate:types': readonly ["--output", "--path", "--tables", "--database", "--chdb-path"];
|
|
42
42
|
readonly 'generate:datasets': readonly ["--output", "--path", "--tables", "--exclude-tables", "--tenant-column"];
|
|
43
43
|
readonly 'generate:manifest': readonly ["--output"];
|
|
44
|
-
readonly 'generate:client': readonly ["--output"];
|
|
44
|
+
readonly 'generate:client': readonly ["--output", "--project", "--environment", "--endpoint"];
|
|
45
45
|
readonly login: readonly ["--cloud-url", "--environment"];
|
|
46
46
|
readonly logout: readonly [];
|
|
47
|
-
readonly deploy: readonly ["--bundle-output", "--release-output", "--project", "--environment", "--endpoint", "--draft"];
|
|
47
|
+
readonly deploy: readonly ["--bundle-output", "--release-output", "--project", "--environment", "--endpoint", "--draft", "--client"];
|
|
48
48
|
readonly 'deployment:build': readonly ["--bundle-output"];
|
|
49
49
|
readonly 'deployment:validate': readonly [];
|
|
50
50
|
readonly 'deployment:release': readonly ["--project", "--environment", "--output"];
|
|
@@ -24,10 +24,10 @@ export const COMMAND_FLAGS = {
|
|
|
24
24
|
'generate:types': ['--output', '--path', '--tables', '--database', '--chdb-path'],
|
|
25
25
|
'generate:datasets': ['--output', '--path', '--tables', '--exclude-tables', '--tenant-column', '--force', '--check', '--diff'],
|
|
26
26
|
'generate:manifest': ['--output'],
|
|
27
|
-
'generate:client': ['--output', '--check', '--force'],
|
|
27
|
+
'generate:client': ['--output', '--check', '--force', '--cloud', '--project', '--environment', '--endpoint'],
|
|
28
28
|
login: ['--cloud-url', '--environment'],
|
|
29
29
|
logout: [],
|
|
30
|
-
deploy: ['--bundle-output', '--release-output', '--project', '--environment', '--no-source', '--endpoint', '--replace-restored', '--mcp-config', '--draft'],
|
|
30
|
+
deploy: ['--bundle-output', '--release-output', '--project', '--environment', '--no-source', '--endpoint', '--replace-restored', '--mcp-config', '--draft', '--client'],
|
|
31
31
|
'deployment:build': ['--bundle-output', '--no-source'],
|
|
32
32
|
'deployment:validate': [],
|
|
33
33
|
'deployment:release': ['--project', '--environment', '--output'],
|
|
@@ -47,10 +47,10 @@ export const VALUE_FLAGS = {
|
|
|
47
47
|
'generate:types': ['--output', '--path', '--tables', '--database', '--chdb-path'],
|
|
48
48
|
'generate:datasets': ['--output', '--path', '--tables', '--exclude-tables', '--tenant-column'],
|
|
49
49
|
'generate:manifest': ['--output'],
|
|
50
|
-
'generate:client': ['--output'],
|
|
50
|
+
'generate:client': ['--output', '--project', '--environment', '--endpoint'],
|
|
51
51
|
login: ['--cloud-url', '--environment'],
|
|
52
52
|
logout: [],
|
|
53
|
-
deploy: ['--bundle-output', '--release-output', '--project', '--environment', '--endpoint', '--draft'],
|
|
53
|
+
deploy: ['--bundle-output', '--release-output', '--project', '--environment', '--endpoint', '--draft', '--client'],
|
|
54
54
|
'deployment:build': ['--bundle-output'],
|
|
55
55
|
'deployment:validate': [],
|
|
56
56
|
'deployment:release': ['--project', '--environment', '--output'],
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProtocolDeploymentContract } from '@hypequery/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a column name is worth presenting as a possible tenant key.
|
|
4
|
+
*
|
|
5
|
+
* A naming match is never enough to activate tenant enforcement. `tenantKey`
|
|
6
|
+
* is a security boundary that must be chosen explicitly by the application.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isTenantKeyCandidateName(name: string): boolean;
|
|
9
|
+
export interface UnscopedTenantCandidate {
|
|
10
|
+
readonly dataset: string;
|
|
11
|
+
readonly columns: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Served datasets about to be published with a customer-like column that
|
|
15
|
+
* neither the dataset nor its endpoint scopes by tenant. Datasets without an
|
|
16
|
+
* endpoint are not callable, so they are skipped.
|
|
17
|
+
*/
|
|
18
|
+
export declare function unscopedTenantCandidates(contract: ProtocolDeploymentContract): UnscopedTenantCandidate[];
|
|
19
|
+
//# sourceMappingURL=tenant-key-candidates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-key-candidates.d.ts","sourceRoot":"","sources":["../../src/utils/tenant-key-candidates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AActE;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAoCD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,0BAA0B,GAAG,uBAAuB,EAAE,CAMxG"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Kept in step with Cloud's customer-scope review, so the CLI and onboarding flag the same columns.
|
|
2
|
+
const CANDIDATE_NAMES = new Set([
|
|
3
|
+
'tenant_id',
|
|
4
|
+
'org_id',
|
|
5
|
+
'organization_id',
|
|
6
|
+
'organisation_id',
|
|
7
|
+
'account_id',
|
|
8
|
+
'customer_id',
|
|
9
|
+
'workspace_id',
|
|
10
|
+
'company_id',
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* Whether a column name is worth presenting as a possible tenant key.
|
|
14
|
+
*
|
|
15
|
+
* A naming match is never enough to activate tenant enforcement. `tenantKey`
|
|
16
|
+
* is a security boundary that must be chosen explicitly by the application.
|
|
17
|
+
*/
|
|
18
|
+
export function isTenantKeyCandidateName(name) {
|
|
19
|
+
return CANDIDATE_NAMES.has(name.toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
const lastSegment = (name) => name.slice(name.lastIndexOf('.') + 1);
|
|
22
|
+
/**
|
|
23
|
+
* The physical columns a dataset reads from its own table, which are the only
|
|
24
|
+
* values `tenantKey` accepts. Dimension sources may be table-qualified or SQL
|
|
25
|
+
* with declared dependencies; measure and filter fields may name a dimension,
|
|
26
|
+
* which resolves to its source. Fields reached through a relationship belong
|
|
27
|
+
* to another table and are skipped.
|
|
28
|
+
*/
|
|
29
|
+
function ownColumns(dataset) {
|
|
30
|
+
const related = new Set((dataset.relationships ?? []).map((relationship) => String(relationship.name)));
|
|
31
|
+
const own = (reference) => {
|
|
32
|
+
const dot = reference.lastIndexOf('.');
|
|
33
|
+
return dot < 0 || !related.has(reference.slice(0, dot));
|
|
34
|
+
};
|
|
35
|
+
const sourceColumns = (dimension) => (dimension.source.kind === 'column' ? [dimension.source.column] : dimension.source.dependencies.map(String))
|
|
36
|
+
.filter(own)
|
|
37
|
+
.map(lastSegment);
|
|
38
|
+
const dimensions = new Map(dataset.dimensions.map((dimension) => [String(dimension.name), dimension]));
|
|
39
|
+
const resolve = (field) => {
|
|
40
|
+
if (!own(field))
|
|
41
|
+
return [];
|
|
42
|
+
const dimension = dimensions.get(lastSegment(field));
|
|
43
|
+
return dimension ? sourceColumns(dimension) : [lastSegment(field)];
|
|
44
|
+
};
|
|
45
|
+
return [
|
|
46
|
+
...dataset.dimensions.flatMap(sourceColumns),
|
|
47
|
+
...dataset.measures.flatMap((measure) => 'field' in measure ? resolve(String(measure.field)) : []),
|
|
48
|
+
...dataset.filters.flatMap((filter) => resolve(String(filter.field))),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Served datasets about to be published with a customer-like column that
|
|
53
|
+
* neither the dataset nor its endpoint scopes by tenant. Datasets without an
|
|
54
|
+
* endpoint are not callable, so they are skipped.
|
|
55
|
+
*/
|
|
56
|
+
export function unscopedTenantCandidates(contract) {
|
|
57
|
+
return contract.datasets.flatMap((dataset) => {
|
|
58
|
+
if (!dataset.endpoint || dataset.tenant.kind === 'required' || dataset.endpoint.tenant.kind === 'required')
|
|
59
|
+
return [];
|
|
60
|
+
const columns = [...new Set(ownColumns(dataset).filter(isTenantKeyCandidateName))];
|
|
61
|
+
return columns.length ? [{ dataset: dataset.name, columns }] : [];
|
|
62
|
+
});
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"description": "CLI for scaffolding type-safe ClickHouse analytics, semantic datasets, React manifests, and deployments",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"clickhouse",
|
|
@@ -23,15 +23,15 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@clickhouse/client": "^1.24.1",
|
|
26
|
-
"chalk": "^
|
|
26
|
+
"chalk": "^6.0.1",
|
|
27
27
|
"commander": "^15.0.0",
|
|
28
28
|
"dotenv": "^18.0.8",
|
|
29
29
|
"esbuild": "^0.28.2",
|
|
30
30
|
"open": "^11.0.4",
|
|
31
|
-
"ora": "^
|
|
31
|
+
"ora": "^9.4.1",
|
|
32
32
|
"prompts": "^2.4.2",
|
|
33
|
-
"@hypequery/deployment": "0.11.
|
|
34
|
-
"@hypequery/protocol": "0.24.
|
|
33
|
+
"@hypequery/deployment": "0.11.2",
|
|
34
|
+
"@hypequery/protocol": "0.24.2"
|
|
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.2",
|
|
58
|
+
"@hypequery/serve": "0.24.2"
|
|
59
59
|
},
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|