@hypequery/cli 1.19.1 → 1.21.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 +22 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -19
- package/dist/commands/deploy.d.ts +3 -2
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +9 -47
- package/dist/commands/deployment.d.ts +1 -9
- package/dist/commands/deployment.d.ts.map +1 -1
- package/dist/commands/deployment.js +34 -169
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +5 -0
- package/dist/templates/cloud.d.ts +3 -0
- package/dist/templates/cloud.d.ts.map +1 -0
- package/dist/templates/cloud.js +11 -0
- package/dist/utils/deployment-bundle.d.ts +2 -8
- package/dist/utils/deployment-bundle.d.ts.map +1 -1
- package/dist/utils/deployment-bundle.js +4 -99
- package/dist/utils/load-cloud-publication.d.ts +4 -0
- package/dist/utils/load-cloud-publication.d.ts.map +1 -0
- package/dist/utils/load-cloud-publication.js +13 -0
- package/package.json +5 -5
- package/dist/utils/cloud-diagnostic.d.ts +0 -10
- package/dist/utils/cloud-diagnostic.d.ts.map +0 -1
- package/dist/utils/cloud-diagnostic.js +0 -10
- package/dist/utils/deployment-runtime-artifact.d.ts +0 -9
- package/dist/utils/deployment-runtime-artifact.d.ts.map +0 -1
- package/dist/utils/deployment-runtime-artifact.js +0 -162
package/README.md
CHANGED
|
@@ -18,8 +18,29 @@ For a semantic layer:
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
npx hypequery init --style datasets
|
|
21
|
+
npx hypequery login
|
|
22
|
+
npx hypequery deploy analytics/cloud.ts
|
|
21
23
|
```
|
|
22
24
|
|
|
25
|
+
The dataset scaffold writes `analytics/cloud.ts` alongside the local
|
|
26
|
+
`analytics/api.ts`. Its Cloud publication is explicit:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { publishToCloud } from '@hypequery/datasets';
|
|
30
|
+
import { datasets } from './datasets.js';
|
|
31
|
+
|
|
32
|
+
export const cloud = publishToCloud({
|
|
33
|
+
datasets,
|
|
34
|
+
access: { roles: [], scopes: [] },
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`api.ts` remains the local Serve entrypoint. Cloud deployment builds from
|
|
39
|
+
`cloud.ts` and does not load Serve configuration. This first Cloud publisher
|
|
40
|
+
accepts datasets only; it rejects named metrics rather than dropping them.
|
|
41
|
+
The required access policy makes the authenticated roles and scopes visible
|
|
42
|
+
before an existing project moves away from Serve-based publishing.
|
|
43
|
+
|
|
23
44
|
For embedded analytics without a ClickHouse server:
|
|
24
45
|
|
|
25
46
|
```bash
|
|
@@ -38,7 +59,7 @@ npx hypequery init \
|
|
|
38
59
|
| `hypequery generate:datasets` | Dataset definitions scaffolded from tables |
|
|
39
60
|
| `hypequery generate:manifest` | A browser-safe route manifest for React hooks |
|
|
40
61
|
| `hypequery login` | An authenticated Cloud target |
|
|
41
|
-
| `hypequery deploy` | A verified
|
|
62
|
+
| `hypequery deploy analytics/cloud.ts` | A verified dataset-only Cloud deployment |
|
|
42
63
|
| `hypequery pull` / `diff` | Live source inspection and comparison |
|
|
43
64
|
|
|
44
65
|
Non-interactive ClickHouse commands read `CLICKHOUSE_URL`, `CLICKHOUSE_DATABASE`, `CLICKHOUSE_USERNAME`, and `CLICKHOUSE_PASSWORD`.
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAqQD,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -167,24 +167,16 @@ program
|
|
|
167
167
|
await generateManifestCommand(api, options);
|
|
168
168
|
}));
|
|
169
169
|
program
|
|
170
|
-
.command('deployment:build <
|
|
171
|
-
.description('Build a verified deployment bundle')
|
|
170
|
+
.command('deployment:build <cloud>')
|
|
171
|
+
.description('Build a verified dataset-only Cloud deployment bundle')
|
|
172
172
|
.option('--bundle-output <directory>', 'Bundle directory (default: analytics/hypequery-deployment)')
|
|
173
|
-
.option('-o, --output <path>', 'Output JSON file (default: analytics/hypequery-deployment.json)')
|
|
174
|
-
.option('--runtime <runtime>', 'Runtime for non-portable handlers: node or python (default: node)')
|
|
175
|
-
.option('--runtime-artifact <sha256>', 'Use a prebuilt runtime artifact with this SHA-256 identity')
|
|
176
|
-
.option('--runtime-file <path>', 'Include the bytes for a prebuilt runtime artifact')
|
|
177
|
-
.option('--runtime-output <path>', 'Bundled Node runtime path (default: beside deployment JSON)')
|
|
178
|
-
.option('--entrypoint-prefix <prefix>', 'Runtime entrypoint prefix (default: queries)')
|
|
179
|
-
.option('--hash-output <path>', 'Deployment identity sidecar path (default: <output>.sha256)')
|
|
180
173
|
.option('--no-source', 'Exclude project source files from the deployment bundle')
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
await buildDeploymentCommand(api, options);
|
|
174
|
+
.action(runCommand(async (cloud, options) => {
|
|
175
|
+
await buildDeploymentCommand(cloud, options);
|
|
184
176
|
}));
|
|
185
177
|
program
|
|
186
178
|
.command('deployment:validate <artifact>')
|
|
187
|
-
.description('Verify a deployment bundle or validate a
|
|
179
|
+
.description('Verify a deployment bundle or validate a deployment contract JSON file')
|
|
188
180
|
.action(runCommand(async (artifact) => {
|
|
189
181
|
await validateDeploymentCommand(artifact);
|
|
190
182
|
}));
|
|
@@ -208,16 +200,14 @@ program
|
|
|
208
200
|
}));
|
|
209
201
|
program
|
|
210
202
|
.command('deploy <source>')
|
|
211
|
-
.description('Build, prepare, and deploy a
|
|
203
|
+
.description('Build, prepare, and deploy a dataset-only Cloud publication')
|
|
212
204
|
.option('--bundle-output <directory>', 'Bundle directory (default: analytics/hypequery-deployment)')
|
|
213
205
|
.option('--release-output <path>', 'Release JSON path (default: beside the bundle)')
|
|
214
206
|
.option('--project <project>', 'Target project identifier (advanced override)')
|
|
215
207
|
.option('--environment <environment>', 'Target environment identifier (advanced override)')
|
|
216
|
-
.option('--release <path>', 'Submit a prebuilt bundle with this release (legacy)')
|
|
217
208
|
.option('--no-source', 'Exclude project source files from the deployment bundle')
|
|
218
209
|
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
219
210
|
.option('--replace-restored', 'Intentionally replace a restored live release')
|
|
220
|
-
.option('--allow-unsupported-config', 'Deploy even though some Serve config will not be honoured in Cloud')
|
|
221
211
|
.action(runCommand(async (source, options) => {
|
|
222
212
|
await deployCommand(source, options);
|
|
223
213
|
}));
|
|
@@ -253,9 +243,9 @@ program.on('--help', () => {
|
|
|
253
243
|
console.log(' hypequery generate:types --output analytics/schema.ts');
|
|
254
244
|
console.log(' hypequery generate:datasets');
|
|
255
245
|
console.log(' hypequery generate:manifest analytics/api.ts --output analytics/hypequery-manifest.json');
|
|
256
|
-
console.log(' hypequery deploy analytics/
|
|
257
|
-
console.log(' hypequery deploy analytics/
|
|
258
|
-
console.log(' hypequery deployment:build analytics/
|
|
246
|
+
console.log(' hypequery deploy analytics/cloud.ts');
|
|
247
|
+
console.log(' hypequery deploy analytics/cloud.ts --project my-project --environment production');
|
|
248
|
+
console.log(' hypequery deployment:build analytics/cloud.ts');
|
|
259
249
|
console.log(' hypequery deployment:validate analytics/hypequery-deployment');
|
|
260
250
|
console.log(' hypequery deployment:release analytics/hypequery-deployment --project my-project --environment production');
|
|
261
251
|
console.log(' hypequery deployment:submit analytics/hypequery-deployment --release analytics/hypequery-deployment.release.json');
|
|
@@ -7,13 +7,14 @@ export interface SubmitDeploymentOptions {
|
|
|
7
7
|
endpoint?: string;
|
|
8
8
|
replaceRestored?: boolean;
|
|
9
9
|
}
|
|
10
|
-
export interface DeployOptions
|
|
10
|
+
export interface DeployOptions {
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
replaceRestored?: boolean;
|
|
11
13
|
project?: string;
|
|
12
14
|
environment?: string;
|
|
13
15
|
bundleOutput?: string;
|
|
14
16
|
releaseOutput?: string;
|
|
15
17
|
source?: boolean;
|
|
16
|
-
allowUnsupportedConfig?: boolean;
|
|
17
18
|
}
|
|
18
19
|
export interface SubmitDeploymentDependencies {
|
|
19
20
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
@@ -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;AAEvC,OAAO,EACL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,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;CAC3B;AAED,MAAM,WAAW,
|
|
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;AAEvC,OAAO,EACL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,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;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,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;CAClB;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;CACjD;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,CA0EvC;AAsBD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CAuCvC"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -123,7 +123,7 @@ export async function submitDeploymentCommand(bundlePath, options = {}, dependen
|
|
|
123
123
|
return result;
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
* `deploy` takes
|
|
126
|
+
* `deploy` takes a Cloud publication module, but the deprecated form took a bundle
|
|
127
127
|
* directory. Dropping `--release` from an existing script would otherwise hand
|
|
128
128
|
* a bundle to the module loader and fail inside esbuild.
|
|
129
129
|
*/
|
|
@@ -135,47 +135,15 @@ async function rejectBundleDirectorySource(sourcePath) {
|
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
throw new Error(`Cannot deploy a prebuilt bundle directory: ${sourcePath}\n\n`
|
|
138
|
-
+ '`hypequery deploy` takes
|
|
139
|
-
+ '`hypequery deploy analytics/
|
|
138
|
+
+ '`hypequery deploy` takes a Cloud publication module, for example '
|
|
139
|
+
+ '`hypequery deploy analytics/cloud.ts`.\n'
|
|
140
140
|
+ 'To upload this bundle, use `hypequery deployment:submit '
|
|
141
141
|
+ `${sourcePath} --release <file>\`.`);
|
|
142
142
|
}
|
|
143
|
-
/**
|
|
144
|
-
* `deploy` exposes no runtime flags, so a runtime error from the shared build
|
|
145
|
-
* command would otherwise point at flags this command does not accept.
|
|
146
|
-
*/
|
|
147
|
-
function withRuntimeFlagHint(error) {
|
|
148
|
-
if (!(error instanceof Error) || !error.message.includes('--runtime'))
|
|
149
|
-
return error;
|
|
150
|
-
return new Error(`${error.message}\n\n`
|
|
151
|
-
+ 'Runtime overrides are not available on `hypequery deploy`. Build with '
|
|
152
|
-
+ '`hypequery deployment:build` (which accepts --runtime, --runtime-artifact, '
|
|
153
|
-
+ 'and --runtime-file), then upload with `hypequery deployment:submit`.');
|
|
154
|
-
}
|
|
155
|
-
function rejectLegacyOrchestrationOptions(options) {
|
|
156
|
-
if (options.project !== undefined
|
|
157
|
-
|| options.environment !== undefined
|
|
158
|
-
|| options.bundleOutput !== undefined
|
|
159
|
-
|| options.releaseOutput !== undefined) {
|
|
160
|
-
throw new Error('--release selects prebuilt submission mode and cannot be combined with '
|
|
161
|
-
+ '--project, --environment, --bundle-output, or --release-output. '
|
|
162
|
-
+ 'Use `hypequery deployment:submit` for explicit prebuilt uploads.');
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
143
|
export async function deployCommand(sourcePath, options = {}, dependencies = {}) {
|
|
166
144
|
if (!sourcePath) {
|
|
167
|
-
throw new Error('Missing
|
|
168
|
-
+ 'Usage: hypequery deploy analytics/
|
|
169
|
-
}
|
|
170
|
-
if (options.release !== undefined) {
|
|
171
|
-
rejectLegacyOrchestrationOptions(options);
|
|
172
|
-
logger.warn('`hypequery deploy <bundle> --release <file>` is deprecated. '
|
|
173
|
-
+ 'Use `hypequery deployment:submit <bundle> --release <file>` instead.');
|
|
174
|
-
return submitDeploymentCommand(sourcePath, {
|
|
175
|
-
release: options.release,
|
|
176
|
-
endpoint: options.endpoint,
|
|
177
|
-
replaceRestored: options.replaceRestored,
|
|
178
|
-
}, dependencies);
|
|
145
|
+
throw new Error('Missing Cloud publication module path.\n\n'
|
|
146
|
+
+ 'Usage: hypequery deploy analytics/cloud.ts');
|
|
179
147
|
}
|
|
180
148
|
await rejectBundleDirectorySource(sourcePath);
|
|
181
149
|
const bundlePath = options.bundleOutput ?? DEFAULT_BUNDLE_PATH;
|
|
@@ -188,16 +156,10 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
|
|
|
188
156
|
// Resolve credentials up front. Submission is the last step, so without this
|
|
189
157
|
// a missing or expired login only surfaces after a full bundle build.
|
|
190
158
|
await resolveDeploymentCredential(options.endpoint, dependencies);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
allowUnsupportedConfig: options.allowUnsupportedConfig,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
catch (error) {
|
|
199
|
-
throw withRuntimeFlagHint(error);
|
|
200
|
-
}
|
|
159
|
+
await build(sourcePath, {
|
|
160
|
+
bundleOutput: bundlePath,
|
|
161
|
+
source: options.source,
|
|
162
|
+
});
|
|
201
163
|
await prepareRelease(bundlePath, {
|
|
202
164
|
project: options.project,
|
|
203
165
|
environment: options.environment,
|
|
@@ -2,15 +2,7 @@ import { type ProtocolDeploymentContract, type ProtocolDeploymentReleaseEnvelope
|
|
|
2
2
|
import { type StoredCloudCredential } from '../utils/cloud-credential-store.js';
|
|
3
3
|
export interface BuildDeploymentOptions {
|
|
4
4
|
bundleOutput?: string;
|
|
5
|
-
output?: string;
|
|
6
|
-
runtime?: 'node' | 'python';
|
|
7
|
-
runtimeArtifact?: string;
|
|
8
|
-
runtimeFile?: string;
|
|
9
|
-
runtimeOutput?: string;
|
|
10
|
-
entrypointPrefix?: string;
|
|
11
|
-
hashOutput?: string;
|
|
12
5
|
source?: boolean;
|
|
13
|
-
allowUnsupportedConfig?: boolean;
|
|
14
6
|
}
|
|
15
7
|
export interface PrepareDeploymentReleaseOptions {
|
|
16
8
|
project?: string;
|
|
@@ -25,7 +17,7 @@ export interface PrepareDeploymentReleaseDependencies {
|
|
|
25
17
|
*/
|
|
26
18
|
outputFlagLabel?: string;
|
|
27
19
|
}
|
|
28
|
-
export declare function buildDeploymentCommand(
|
|
20
|
+
export declare function buildDeploymentCommand(sourcePath: string | undefined, options?: BuildDeploymentOptions): Promise<ProtocolDeploymentContract>;
|
|
29
21
|
export declare function validateDeploymentCommand(artifactPath: string | undefined): Promise<ProtocolDeploymentContract>;
|
|
30
22
|
export declare function prepareDeploymentReleaseCommand(bundlePath: string | undefined, options?: PrepareDeploymentReleaseOptions, dependencies?: PrepareDeploymentReleaseDependencies): Promise<ProtocolDeploymentReleaseEnvelope>;
|
|
31
23
|
//# sourceMappingURL=deployment.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"
|
|
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;AAE5C,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;AAID,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAkCrC;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"}
|
|
@@ -1,177 +1,44 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { lstat, mkdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { prepareProtocolDeploymentReleaseEnvelope, prepareProtocolDeploymentContract, } from '@hypequery/protocol';
|
|
5
|
-
import { writeDeploymentBundle, verifyDeploymentBundle,
|
|
6
|
-
import { buildNodeRuntimeArtifact, getDeploymentRuntimeEntrypoints, } from '../utils/deployment-runtime-artifact.js';
|
|
4
|
+
import { writeDeploymentBundle, verifyDeploymentBundle, } from '../utils/deployment-bundle.js';
|
|
7
5
|
import { captureDeploymentSourceSnapshot } from '../utils/deployment-source-snapshot.js';
|
|
8
|
-
import {
|
|
6
|
+
import { loadCloudPublication } from '../utils/load-cloud-publication.js';
|
|
9
7
|
import { logger } from '../utils/logger.js';
|
|
10
|
-
import { reportCloudDiagnostic } from '../utils/cloud-diagnostic.js';
|
|
11
8
|
import { loadCloudCredential, } from '../utils/cloud-credential-store.js';
|
|
12
|
-
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
13
9
|
const DEFAULT_BUNDLE_OUTPUT = 'analytics/hypequery-deployment';
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
export async function buildDeploymentCommand(sourcePath, options = {}) {
|
|
11
|
+
if (!sourcePath) {
|
|
12
|
+
throw new Error('Missing Cloud publication module path.\n\n'
|
|
13
|
+
+ 'Usage: hypequery deployment:build analytics/cloud.ts');
|
|
14
|
+
}
|
|
15
|
+
const bundleOutput = options.bundleOutput ?? DEFAULT_BUNDLE_OUTPUT;
|
|
16
|
+
const contract = await loadCloudPublication(sourcePath);
|
|
17
|
+
const candidate = contract;
|
|
18
|
+
if ((Array.isArray(candidate.queries) && candidate.queries.length > 0)
|
|
19
|
+
|| (Array.isArray(candidate.artifacts) && candidate.artifacts.length > 0)) {
|
|
20
|
+
throw new Error('Cloud publication modules may publish datasets only.');
|
|
18
21
|
}
|
|
19
|
-
return runtime;
|
|
20
|
-
}
|
|
21
|
-
function configuredRuntimeArtifact(options, runtime) {
|
|
22
|
-
if (options.runtimeArtifact === undefined)
|
|
23
|
-
return undefined;
|
|
24
|
-
if (!SHA256_PATTERN.test(options.runtimeArtifact)) {
|
|
25
|
-
throw new Error('--runtime-artifact must be a lowercase 64-character SHA-256 digest.');
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
runtime,
|
|
29
|
-
artifactSha256: options.runtimeArtifact,
|
|
30
|
-
...(options.entrypointPrefix !== undefined
|
|
31
|
-
? { entrypointPrefix: options.entrypointPrefix }
|
|
32
|
-
: {}),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function assertDistinctOutputPaths(paths) {
|
|
36
|
-
const seen = new Map();
|
|
37
|
-
for (const [label, value] of Object.entries(paths)) {
|
|
38
|
-
if (value === undefined)
|
|
39
|
-
continue;
|
|
40
|
-
const resolved = path.resolve(value);
|
|
41
|
-
const existing = seen.get(resolved);
|
|
42
|
-
if (existing) {
|
|
43
|
-
throw new Error(`${label} must use a different path from ${existing}.`);
|
|
44
|
-
}
|
|
45
|
-
seen.set(resolved, label);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
export async function buildDeploymentCommand(apiPath, options = {}) {
|
|
49
|
-
if (!apiPath) {
|
|
50
|
-
throw new Error('Missing API module path.\n\n'
|
|
51
|
-
+ 'Usage: hypequery deployment:build analytics/api.ts');
|
|
52
|
-
}
|
|
53
|
-
const runtime = runtimeName(options);
|
|
54
|
-
const configuredArtifact = configuredRuntimeArtifact(options, runtime);
|
|
55
|
-
const legacyOutputRequested = options.output !== undefined
|
|
56
|
-
|| options.hashOutput !== undefined
|
|
57
|
-
|| options.runtimeOutput !== undefined;
|
|
58
|
-
if (options.bundleOutput !== undefined && legacyOutputRequested) {
|
|
59
|
-
throw new Error('--bundle-output cannot be combined with --output, --hash-output, or --runtime-output.');
|
|
60
|
-
}
|
|
61
|
-
const bundleOutput = options.bundleOutput
|
|
62
|
-
?? (legacyOutputRequested ? undefined : DEFAULT_BUNDLE_OUTPUT);
|
|
63
|
-
if (options.runtimeFile !== undefined && configuredArtifact === undefined) {
|
|
64
|
-
throw new Error('--runtime-file requires --runtime-artifact.');
|
|
65
|
-
}
|
|
66
|
-
if (options.runtimeFile !== undefined && bundleOutput === undefined) {
|
|
67
|
-
throw new Error('--runtime-file is only supported when building a deployment bundle.');
|
|
68
|
-
}
|
|
69
|
-
if (configuredArtifact && bundleOutput !== undefined && options.runtimeFile === undefined) {
|
|
70
|
-
throw new Error('A complete deployment bundle requires --runtime-file with a prebuilt --runtime-artifact.');
|
|
71
|
-
}
|
|
72
|
-
if (configuredArtifact && options.runtimeOutput !== undefined) {
|
|
73
|
-
throw new Error('--runtime-output cannot be used with a prebuilt --runtime-artifact.');
|
|
74
|
-
}
|
|
75
|
-
const outputPath = options.output ?? 'analytics/hypequery-deployment.json';
|
|
76
|
-
const hashOutputPath = options.hashOutput ?? `${outputPath}.sha256`;
|
|
77
|
-
if (options.runtimeOutput !== undefined) {
|
|
78
|
-
assertDistinctOutputPaths({
|
|
79
|
-
'--output': outputPath,
|
|
80
|
-
'--hash-output': hashOutputPath,
|
|
81
|
-
'--runtime-output': options.runtimeOutput,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
const api = await loadApiModule(apiPath);
|
|
85
|
-
if (typeof api.deploymentContract !== 'function') {
|
|
86
|
-
throw new Error(`Invalid API module: ${apiPath}\n\n`
|
|
87
|
-
+ 'The exported API must provide deploymentContract(). '
|
|
88
|
-
+ 'Upgrade @hypequery/serve and export the value returned by createAPI() or serve().');
|
|
89
|
-
}
|
|
90
|
-
let builtArtifact;
|
|
91
|
-
let artifact = configuredArtifact;
|
|
92
|
-
if (!artifact) {
|
|
93
|
-
const runtimeEntrypoints = getDeploymentRuntimeEntrypoints(api);
|
|
94
|
-
if (runtimeEntrypoints.length > 0) {
|
|
95
|
-
if (runtime !== 'node') {
|
|
96
|
-
throw new Error('Automatic runtime artifact builds currently support Node only. '
|
|
97
|
-
+ 'Provide --runtime-artifact for Python deployments.');
|
|
98
|
-
}
|
|
99
|
-
builtArtifact = await buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, options.entrypointPrefix);
|
|
100
|
-
artifact = {
|
|
101
|
-
runtime: 'node',
|
|
102
|
-
artifactSha256: builtArtifact.artifactSha256,
|
|
103
|
-
entrypointPrefix: builtArtifact.entrypointPrefix,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const contract = api.deploymentContract({
|
|
108
|
-
...(artifact ? { runtimeArtifact: artifact } : {}),
|
|
109
|
-
...(options.allowUnsupportedConfig ? { allowUnsupportedConfig: true } : {}),
|
|
110
|
-
onCloudDiagnostic: diagnostic => reportCloudDiagnostic(diagnostic, options.allowUnsupportedConfig === true),
|
|
111
|
-
});
|
|
112
22
|
const prepared = prepareProtocolDeploymentContract(contract);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const sourceSnapshot = options.source === false
|
|
133
|
-
? undefined
|
|
134
|
-
: await captureDeploymentSourceSnapshot(apiPath);
|
|
135
|
-
const bundle = await writeDeploymentBundle(bundleOutput, prepared, runtimeFiles, sourceSnapshot);
|
|
136
|
-
logger.success(`Deployment bundle written to ${bundle.directory}`);
|
|
137
|
-
if (bundle.manifest.source) {
|
|
138
|
-
logger.info(`Captured ${bundle.manifest.source.files.length} source `
|
|
139
|
-
+ `${bundle.manifest.source.files.length === 1 ? 'file' : 'files'}`);
|
|
140
|
-
}
|
|
141
|
-
logger.info(`Bundle identity: ${bundle.identity}`);
|
|
142
|
-
logger.info(`Deployment identity: ${digest}`);
|
|
143
|
-
return validated;
|
|
144
|
-
}
|
|
145
|
-
const runtimeOutputPath = builtArtifact
|
|
146
|
-
? options.runtimeOutput ?? path.join(path.dirname(outputPath), 'hypequery-runtime.mjs')
|
|
147
|
-
: undefined;
|
|
148
|
-
assertDistinctOutputPaths({
|
|
149
|
-
'--output': outputPath,
|
|
150
|
-
'--hash-output': hashOutputPath,
|
|
151
|
-
'--runtime-output': runtimeOutputPath,
|
|
152
|
-
});
|
|
153
|
-
const identitySidecar = [
|
|
154
|
-
'# Hypequery deployment identity v1; not a file checksum or sha256sum input.',
|
|
155
|
-
'# SHA-256(UTF-8("hypequery:deployment:v1") || 0x00 || RFC 8785 canonical bytes); '
|
|
156
|
-
+ 'the output newline is excluded.',
|
|
157
|
-
`${digest} ${path.basename(outputPath)}`,
|
|
158
|
-
'',
|
|
159
|
-
].join('\n');
|
|
160
|
-
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
161
|
-
await mkdir(path.dirname(hashOutputPath), { recursive: true });
|
|
162
|
-
if (runtimeOutputPath && builtArtifact) {
|
|
163
|
-
await mkdir(path.dirname(runtimeOutputPath), { recursive: true });
|
|
164
|
-
await writeFile(runtimeOutputPath, builtArtifact.bytes);
|
|
165
|
-
}
|
|
166
|
-
await writeFile(outputPath, `${canonical}\n`, 'utf8');
|
|
167
|
-
await writeFile(hashOutputPath, identitySidecar, 'utf8');
|
|
168
|
-
if (runtimeOutputPath && builtArtifact) {
|
|
169
|
-
logger.success(`Runtime artifact written to ${runtimeOutputPath}`);
|
|
170
|
-
logger.info(`Runtime artifact SHA-256: ${builtArtifact.artifactSha256}`);
|
|
171
|
-
}
|
|
172
|
-
logger.success(`Deployment contract written to ${outputPath}`);
|
|
173
|
-
logger.info(`Identity: ${digest}`);
|
|
174
|
-
return validated;
|
|
23
|
+
const sourceSnapshot = options.source === false
|
|
24
|
+
? undefined
|
|
25
|
+
: await captureDeploymentSourceSnapshot(sourcePath);
|
|
26
|
+
const bundle = await writeDeploymentBundle(bundleOutput, prepared, sourceSnapshot);
|
|
27
|
+
logger.success(`Deployment bundle written to ${bundle.directory}`);
|
|
28
|
+
if (bundle.manifest.source) {
|
|
29
|
+
logger.info(`Captured ${bundle.manifest.source.files.length} source `
|
|
30
|
+
+ `${bundle.manifest.source.files.length === 1 ? 'file' : 'files'}`);
|
|
31
|
+
}
|
|
32
|
+
logger.info(`${prepared.contract.datasets.length} `
|
|
33
|
+
+ `${prepared.contract.datasets.length === 1 ? 'dataset' : 'datasets'}, 0 runtime artifacts`);
|
|
34
|
+
logger.info(`Bundle identity: ${bundle.identity}`);
|
|
35
|
+
logger.info(`Deployment identity: ${prepared.identity}`);
|
|
36
|
+
return prepared.contract;
|
|
37
|
+
}
|
|
38
|
+
function describeContract(contract) {
|
|
39
|
+
const datasets = `${contract.datasets.length} `
|
|
40
|
+
+ `${contract.datasets.length === 1 ? 'dataset' : 'datasets'}`;
|
|
41
|
+
return datasets;
|
|
175
42
|
}
|
|
176
43
|
export async function validateDeploymentCommand(artifactPath) {
|
|
177
44
|
if (!artifactPath) {
|
|
@@ -194,8 +61,7 @@ export async function validateDeploymentCommand(artifactPath) {
|
|
|
194
61
|
const bundle = await verifyDeploymentBundle(artifactPath);
|
|
195
62
|
const contract = bundle.contract;
|
|
196
63
|
logger.success(`Valid deployment bundle: ${artifactPath}`);
|
|
197
|
-
logger.info(
|
|
198
|
-
+ `${contract.artifacts.length} runtime artifacts`);
|
|
64
|
+
logger.info(describeContract(contract));
|
|
199
65
|
logger.info(`Bundle identity: ${bundle.identity}`);
|
|
200
66
|
logger.info(`Deployment identity: ${bundle.manifest.deployment.identity}`);
|
|
201
67
|
return contract;
|
|
@@ -226,8 +92,7 @@ export async function validateDeploymentCommand(artifactPath) {
|
|
|
226
92
|
}
|
|
227
93
|
const { contract, identity: digest } = prepared;
|
|
228
94
|
logger.success(`Valid deployment contract: ${artifactPath}`);
|
|
229
|
-
logger.info(
|
|
230
|
-
+ `${contract.artifacts.length} runtime artifacts`);
|
|
95
|
+
logger.info(describeContract(contract));
|
|
231
96
|
logger.info(`Identity: ${digest}`);
|
|
232
97
|
return contract;
|
|
233
98
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAIA,OAAO,EAcL,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAe7B,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAIA,OAAO,EAcL,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAe7B,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAYzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAqKD,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,iBAsb1D"}
|
package/dist/commands/init.js
CHANGED
|
@@ -10,6 +10,7 @@ import { generateEnvTemplate, appendToEnv } from '../templates/env.js';
|
|
|
10
10
|
import { generateClientTemplate } from '../templates/client.js';
|
|
11
11
|
import { generateQueriesTemplate } from '../templates/queries.js';
|
|
12
12
|
import { generateApiTemplate } from '../templates/api.js';
|
|
13
|
+
import { generateCloudTemplate } from '../templates/cloud.js';
|
|
13
14
|
import { CONTEXT_AUTH_TENANT_COLUMN } from '../templates/auth-scaffold.js';
|
|
14
15
|
import { generateDatasetsPlaceholderTemplate } from '../templates/datasets.js';
|
|
15
16
|
import { appendToGitignore } from '../templates/gitignore.js';
|
|
@@ -234,6 +235,7 @@ export async function initCommand(options = {}) {
|
|
|
234
235
|
? [
|
|
235
236
|
path.join(resolvedOutputDir, 'datasets.ts'),
|
|
236
237
|
path.join(resolvedOutputDir, 'api.ts'),
|
|
238
|
+
path.join(resolvedOutputDir, 'cloud.ts'),
|
|
237
239
|
]
|
|
238
240
|
: [
|
|
239
241
|
path.join(resolvedOutputDir, 'queries.ts'),
|
|
@@ -421,6 +423,9 @@ export interface IntrospectedSchema {
|
|
|
421
423
|
apiPath = path.join(resolvedOutputDir, 'api.ts');
|
|
422
424
|
await writeFile(apiPath, generateApiTemplate({ auth }));
|
|
423
425
|
logger.success(`Created API file (${path.relative(process.cwd(), apiPath)})`);
|
|
426
|
+
const cloudPath = path.join(resolvedOutputDir, 'cloud.ts');
|
|
427
|
+
await writeFile(cloudPath, generateCloudTemplate());
|
|
428
|
+
logger.success(`Created Cloud publication (${path.relative(process.cwd(), cloudPath)})`);
|
|
424
429
|
}
|
|
425
430
|
else {
|
|
426
431
|
apiPath = path.join(resolvedOutputDir, 'queries.ts');
|
|
@@ -0,0 +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,CAS9C"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Generate the explicit Cloud publication entrypoint for dataset projects. */
|
|
2
|
+
export function generateCloudTemplate() {
|
|
3
|
+
return `import { publishToCloud } from '@hypequery/datasets';
|
|
4
|
+
import { datasets } from './datasets.js';
|
|
5
|
+
|
|
6
|
+
export const cloud = publishToCloud({
|
|
7
|
+
datasets,
|
|
8
|
+
access: { roles: [], scopes: [] },
|
|
9
|
+
});
|
|
10
|
+
`;
|
|
11
|
+
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { type PreparedProtocolDeploymentContract, type
|
|
1
|
+
import { type PreparedProtocolDeploymentContract, type ProtocolDeploymentContract, type ProtocolDeploymentBundleManifest } from '@hypequery/protocol';
|
|
2
2
|
import { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle } from '@hypequery/deployment';
|
|
3
3
|
import type { VerifiedDeploymentBundle } from '@hypequery/deployment';
|
|
4
4
|
export { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, };
|
|
5
5
|
export type { VerifiedDeploymentBundle };
|
|
6
|
-
export interface DeploymentBundleRuntimeFile {
|
|
7
|
-
readonly runtime: 'node' | 'python';
|
|
8
|
-
readonly sha256: string;
|
|
9
|
-
readonly bytes: Uint8Array;
|
|
10
|
-
}
|
|
11
6
|
export interface DeploymentBundleSourceFile {
|
|
12
7
|
readonly path: string;
|
|
13
8
|
readonly bytes: Uint8Array;
|
|
@@ -28,6 +23,5 @@ export interface WrittenDeploymentBundle {
|
|
|
28
23
|
readonly identity: string;
|
|
29
24
|
readonly contract: ProtocolDeploymentContract;
|
|
30
25
|
}
|
|
31
|
-
export declare function writeDeploymentBundle(outputDirectory: string, prepared: PreparedProtocolDeploymentContract,
|
|
32
|
-
export declare function readDeploymentRuntimeFile(filePath: string): Promise<Uint8Array>;
|
|
26
|
+
export declare function writeDeploymentBundle(outputDirectory: string, prepared: PreparedProtocolDeploymentContract, sourceSnapshot?: DeploymentBundleSourceSnapshot): Promise<WrittenDeploymentBundle>;
|
|
33
27
|
//# sourceMappingURL=deployment-bundle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,KAAK,kCAAkC,EACvC,KAAK,0BAA0B,EAC/B,KAAK,gCAAgC,EACtC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAGtE,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,GACvB,CAAC;AACF,YAAY,EAAE,wBAAwB,EAAE,CAAC;AAEzC,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,gCAAgC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;CAC/C;AA4ED,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,cAAc,CAAC,EAAE,8BAA8B,GAC9C,OAAO,CAAC,uBAAuB,CAAC,CAkFlC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { lstat, mkdir, mkdtemp, rename, rm, writeFile, } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, prepareProtocolDeploymentBundleManifest, } from '@hypequery/protocol';
|
|
5
5
|
import { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, } from '@hypequery/deployment';
|
|
@@ -9,9 +9,6 @@ const utf8Encoder = new TextEncoder();
|
|
|
9
9
|
function sha256(bytes) {
|
|
10
10
|
return createHash('sha256').update(bytes).digest('hex');
|
|
11
11
|
}
|
|
12
|
-
function runtimeArtifactPath(runtime, digest) {
|
|
13
|
-
return `artifacts/${digest}.${runtime === 'node' ? 'mjs' : 'pyz'}`;
|
|
14
|
-
}
|
|
15
12
|
const SOURCE_ROOT = 'source';
|
|
16
13
|
function errorCode(error) {
|
|
17
14
|
return typeof error === 'object' && error !== null && 'code' in error
|
|
@@ -37,53 +34,6 @@ async function existingVerifiedBundle(outputDirectory) {
|
|
|
37
34
|
}
|
|
38
35
|
return true;
|
|
39
36
|
}
|
|
40
|
-
function validateRuntimeFiles(prepared, files) {
|
|
41
|
-
requireClosedContractArtifactSet(prepared.contract);
|
|
42
|
-
const declared = new Map(prepared.contract.artifacts.map(artifact => [artifact.artifactSha256, artifact.runtime]));
|
|
43
|
-
const supplied = new Map();
|
|
44
|
-
for (const file of files) {
|
|
45
|
-
if (sha256(file.bytes) !== file.sha256) {
|
|
46
|
-
throw new Error(`Runtime artifact bytes do not match SHA-256 ${file.sha256}.`);
|
|
47
|
-
}
|
|
48
|
-
if (declared.get(file.sha256) !== file.runtime) {
|
|
49
|
-
throw new Error(`Runtime artifact ${file.sha256} is not declared by the deployment contract as ${file.runtime}.`);
|
|
50
|
-
}
|
|
51
|
-
if (supplied.has(file.sha256)) {
|
|
52
|
-
throw new Error(`Duplicate runtime artifact bytes supplied for ${file.sha256}.`);
|
|
53
|
-
}
|
|
54
|
-
if (file.bytes.byteLength < 1
|
|
55
|
-
|| file.bytes.byteLength > DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes) {
|
|
56
|
-
throw new Error(`Runtime artifact ${file.sha256} exceeds the bundle byte limits.`);
|
|
57
|
-
}
|
|
58
|
-
supplied.set(file.sha256, file.runtime);
|
|
59
|
-
}
|
|
60
|
-
for (const [digest, runtime] of declared) {
|
|
61
|
-
if (supplied.get(digest) !== runtime) {
|
|
62
|
-
throw new Error(`Deployment bundle is missing ${runtime} runtime artifact ${digest}.`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return Object.freeze([...files].sort((left, right) => {
|
|
66
|
-
const leftPath = runtimeArtifactPath(left.runtime, left.sha256);
|
|
67
|
-
const rightPath = runtimeArtifactPath(right.runtime, right.sha256);
|
|
68
|
-
return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
|
-
function requireClosedContractArtifactSet(contract) {
|
|
72
|
-
const referenced = new Map();
|
|
73
|
-
for (const query of contract.queries) {
|
|
74
|
-
if (query.implementation.kind === 'runtime-reference') {
|
|
75
|
-
referenced.set(query.implementation.artifactSha256, query.implementation.runtime);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (referenced.size !== contract.artifacts.length) {
|
|
79
|
-
throw new Error('Deployment contract contains missing or unreferenced runtime artifacts.');
|
|
80
|
-
}
|
|
81
|
-
for (const artifact of contract.artifacts) {
|
|
82
|
-
if (referenced.get(artifact.artifactSha256) !== artifact.runtime) {
|
|
83
|
-
throw new Error(`Deployment runtime artifact ${artifact.artifactSha256} is not referenced by a named query.`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
37
|
function validateSourceSnapshot(source) {
|
|
88
38
|
if (!source)
|
|
89
39
|
return undefined;
|
|
@@ -122,12 +72,11 @@ function validateSourceSnapshot(source) {
|
|
|
122
72
|
...(source.revision ? { revision: Object.freeze({ ...source.revision }) } : {}),
|
|
123
73
|
});
|
|
124
74
|
}
|
|
125
|
-
export async function writeDeploymentBundle(outputDirectory, prepared,
|
|
75
|
+
export async function writeDeploymentBundle(outputDirectory, prepared, sourceSnapshot) {
|
|
126
76
|
const destination = path.resolve(outputDirectory);
|
|
127
77
|
if (destination === path.parse(destination).root) {
|
|
128
78
|
throw new Error('The deployment bundle output cannot be a filesystem root.');
|
|
129
79
|
}
|
|
130
|
-
const files = validateRuntimeFiles(prepared, runtimeFiles);
|
|
131
80
|
const source = validateSourceSnapshot(sourceSnapshot);
|
|
132
81
|
const deploymentBytes = utf8Encoder.encode(`${prepared.canonical}\n`);
|
|
133
82
|
const manifestInput = {
|
|
@@ -139,12 +88,8 @@ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFi
|
|
|
139
88
|
sha256: sha256(deploymentBytes),
|
|
140
89
|
byteLength: deploymentBytes.byteLength,
|
|
141
90
|
},
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
path: runtimeArtifactPath(file.runtime, file.sha256),
|
|
145
|
-
sha256: file.sha256,
|
|
146
|
-
byteLength: file.bytes.byteLength,
|
|
147
|
-
})),
|
|
91
|
+
// A deployment carries datasets: there is nothing else to put here.
|
|
92
|
+
artifacts: [],
|
|
148
93
|
...(source ? {
|
|
149
94
|
source: {
|
|
150
95
|
root: SOURCE_ROOT,
|
|
@@ -164,12 +109,7 @@ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFi
|
|
|
164
109
|
await mkdir(parent, { recursive: true });
|
|
165
110
|
const staging = await mkdtemp(path.join(parent, `.${path.basename(destination)}.tmp-`));
|
|
166
111
|
try {
|
|
167
|
-
if (files.length > 0)
|
|
168
|
-
await mkdir(path.join(staging, 'artifacts'));
|
|
169
112
|
await writeFile(path.join(staging, DEPLOYMENT_BUNDLE_CONTRACT), deploymentBytes, { flag: 'wx' });
|
|
170
|
-
for (const file of files) {
|
|
171
|
-
await writeFile(path.join(staging, runtimeArtifactPath(file.runtime, file.sha256)), file.bytes, { flag: 'wx' });
|
|
172
|
-
}
|
|
173
113
|
for (const file of source?.files ?? []) {
|
|
174
114
|
const outputPath = path.join(staging, SOURCE_ROOT, ...file.path.split('/'));
|
|
175
115
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
@@ -209,38 +149,3 @@ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFi
|
|
|
209
149
|
contract: prepared.contract,
|
|
210
150
|
});
|
|
211
151
|
}
|
|
212
|
-
async function readBoundedAbsoluteRegularFile(absolutePath, displayPath, maximum) {
|
|
213
|
-
let handle;
|
|
214
|
-
try {
|
|
215
|
-
const initialStat = await lstat(absolutePath);
|
|
216
|
-
if (initialStat.isSymbolicLink()) {
|
|
217
|
-
throw new Error(`Bundle entry must not be a symbolic link: ${displayPath}`);
|
|
218
|
-
}
|
|
219
|
-
if (!initialStat.isFile()) {
|
|
220
|
-
throw new Error(`Bundle entry is not a regular file: ${displayPath}`);
|
|
221
|
-
}
|
|
222
|
-
if (initialStat.size < 1 || initialStat.size > maximum) {
|
|
223
|
-
throw new Error(`Bundle entry exceeds its byte limit: ${displayPath}`);
|
|
224
|
-
}
|
|
225
|
-
handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
226
|
-
const stat = await handle.stat();
|
|
227
|
-
if (!stat.isFile())
|
|
228
|
-
throw new Error(`Bundle entry is not a regular file: ${displayPath}`);
|
|
229
|
-
if (stat.size < 1 || stat.size > maximum) {
|
|
230
|
-
throw new Error(`Bundle entry exceeds its byte limit: ${displayPath}`);
|
|
231
|
-
}
|
|
232
|
-
return await handle.readFile();
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
if (errorCode(error) === 'ELOOP') {
|
|
236
|
-
throw new Error(`Bundle entry must not be a symbolic link: ${displayPath}`);
|
|
237
|
-
}
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
finally {
|
|
241
|
-
await handle?.close();
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
export async function readDeploymentRuntimeFile(filePath) {
|
|
245
|
-
return readBoundedAbsoluteRegularFile(path.resolve(filePath), filePath, DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes);
|
|
246
|
-
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProtocolDeploymentContract } from '@hypequery/protocol';
|
|
2
|
+
/** Load the explicit dataset contract exported by a Cloud publication module. */
|
|
3
|
+
export declare function loadCloudPublication(sourcePath: string): Promise<ProtocolDeploymentContract>;
|
|
4
|
+
//# sourceMappingURL=load-cloud-publication.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-cloud-publication.d.ts","sourceRoot":"","sources":["../../src/utils/load-cloud-publication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAGtE,iFAAiF;AACjF,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAYlG"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { loadModule } from './load-api.js';
|
|
2
|
+
/** Load the explicit dataset contract exported by a Cloud publication module. */
|
|
3
|
+
export async function loadCloudPublication(sourcePath) {
|
|
4
|
+
const module = await loadModule(sourcePath);
|
|
5
|
+
const cloud = module.cloud ?? module.default;
|
|
6
|
+
if (typeof cloud !== 'object' || cloud === null
|
|
7
|
+
|| !('kind' in cloud) || cloud.kind !== 'hypequery-deployment') {
|
|
8
|
+
throw new Error(`Invalid Cloud publication: ${sourcePath}\n\n`
|
|
9
|
+
+ 'Export `cloud = publishToCloud({ datasets, access })` from @hypequery/datasets. '
|
|
10
|
+
+ 'A Serve API is not a Cloud publication.');
|
|
11
|
+
}
|
|
12
|
+
return cloud;
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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": "^10.0.0",
|
|
31
31
|
"ora": "^8.0.1",
|
|
32
32
|
"prompts": "^2.4.2",
|
|
33
|
-
"@hypequery/deployment": "0.
|
|
34
|
-
"@hypequery/protocol": "0.
|
|
33
|
+
"@hypequery/deployment": "0.9.1",
|
|
34
|
+
"@hypequery/protocol": "0.15.0"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
37
|
"@napi-rs/keyring": "1.3.0"
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@vitest/coverage-v8": "^3.2.6",
|
|
52
52
|
"typescript": "^5.7.3",
|
|
53
53
|
"vitest": "^3.2.6",
|
|
54
|
-
"@hypequery/mcp": "0.
|
|
55
|
-
"@hypequery/serve": "0.
|
|
54
|
+
"@hypequery/mcp": "0.8.2",
|
|
55
|
+
"@hypequery/serve": "0.19.0"
|
|
56
56
|
},
|
|
57
57
|
"repository": {
|
|
58
58
|
"type": "git",
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export interface CloudCompatibilityDiagnosticLike {
|
|
2
|
-
readonly severity: 'error' | 'warning';
|
|
3
|
-
readonly code: string;
|
|
4
|
-
readonly subject: string;
|
|
5
|
-
readonly message: string;
|
|
6
|
-
readonly remedy: string;
|
|
7
|
-
}
|
|
8
|
-
/** Report warnings and explicitly overridden errors before submitting a release. */
|
|
9
|
-
export declare function reportCloudDiagnostic(diagnostic: CloudCompatibilityDiagnosticLike, allowUnsupportedConfig: boolean): void;
|
|
10
|
-
//# sourceMappingURL=cloud-diagnostic.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-diagnostic.d.ts","sourceRoot":"","sources":["../../src/utils/cloud-diagnostic.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,gCAAgC,EAC5C,sBAAsB,EAAE,OAAO,GAC9B,IAAI,CAMN"}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { logger } from './logger.js';
|
|
2
|
-
/** Report warnings and explicitly overridden errors before submitting a release. */
|
|
3
|
-
export function reportCloudDiagnostic(diagnostic, allowUnsupportedConfig) {
|
|
4
|
-
// Blocking errors are reported by the contract builder's exception.
|
|
5
|
-
if (diagnostic.severity === 'error' && !allowUnsupportedConfig)
|
|
6
|
-
return;
|
|
7
|
-
logger.warn(`${diagnostic.code} (${diagnostic.subject})`);
|
|
8
|
-
logger.indent(diagnostic.message);
|
|
9
|
-
logger.indent(`→ ${diagnostic.remedy}`);
|
|
10
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export interface NodeRuntimeArtifact {
|
|
2
|
-
readonly bytes: Uint8Array;
|
|
3
|
-
readonly artifactSha256: string;
|
|
4
|
-
readonly entrypointPrefix: string;
|
|
5
|
-
readonly runtimeEntrypoints: readonly string[];
|
|
6
|
-
}
|
|
7
|
-
export declare function getDeploymentRuntimeEntrypoints(input: unknown): readonly string[];
|
|
8
|
-
export declare function buildNodeRuntimeArtifact(apiPath: string, runtimeEntrypoints: readonly string[], entrypointPrefix?: string): Promise<NodeRuntimeArtifact>;
|
|
9
|
-
//# sourceMappingURL=deployment-runtime-artifact.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deployment-runtime-artifact.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-runtime-artifact.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD;AASD,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CA6BjF;AAiED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,kBAAkB,EAAE,SAAS,MAAM,EAAE,EACrC,gBAAgB,SAAY,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CAmD9B"}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { parseProtocolIdentifier, parseProtocolQualifiedIdentifier, } from '@hypequery/protocol';
|
|
4
|
-
import { build } from 'esbuild';
|
|
5
|
-
import { findNearestTsconfig } from './load-api.js';
|
|
6
|
-
const deploymentBuildSourceSymbol = Symbol.for('hypequery.deployment-build-source.v1');
|
|
7
|
-
function executableQuery(query) {
|
|
8
|
-
return typeof query === 'function'
|
|
9
|
-
|| (typeof query === 'object'
|
|
10
|
-
&& query !== null
|
|
11
|
-
&& typeof query.run === 'function');
|
|
12
|
-
}
|
|
13
|
-
export function getDeploymentRuntimeEntrypoints(input) {
|
|
14
|
-
if (typeof input !== 'object' || input === null) {
|
|
15
|
-
throw new Error('Invalid API module: deployment build metadata is unavailable.');
|
|
16
|
-
}
|
|
17
|
-
const api = input;
|
|
18
|
-
const source = api[deploymentBuildSourceSymbol];
|
|
19
|
-
if (source?.version !== 1 || !Array.isArray(source.runtimeEntrypoints)) {
|
|
20
|
-
throw new Error('The exported API does not expose deployment build metadata. '
|
|
21
|
-
+ 'Upgrade @hypequery/serve before building runtime artifacts.');
|
|
22
|
-
}
|
|
23
|
-
const names = source.runtimeEntrypoints.map((name) => {
|
|
24
|
-
try {
|
|
25
|
-
return parseProtocolIdentifier(name);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
throw new Error(`Invalid runtime entrypoint name: ${String(name)}`);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
if (new Set(names).size !== names.length) {
|
|
32
|
-
throw new Error('Deployment build metadata contains duplicate runtime entrypoints.');
|
|
33
|
-
}
|
|
34
|
-
for (const name of names) {
|
|
35
|
-
if (!executableQuery(api.queries?.[name]?.query)) {
|
|
36
|
-
throw new Error(`Serve query "${name}" does not expose an executable runtime handler.`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return Object.freeze([...names].sort());
|
|
40
|
-
}
|
|
41
|
-
function projectRelativeImport(apiPath) {
|
|
42
|
-
const resolved = path.resolve(process.cwd(), apiPath);
|
|
43
|
-
const relative = path.relative(process.cwd(), resolved);
|
|
44
|
-
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
45
|
-
throw new Error('The API module must be inside the current project for deterministic bundling.');
|
|
46
|
-
}
|
|
47
|
-
const portable = relative.split(path.sep).join('/');
|
|
48
|
-
return {
|
|
49
|
-
resolved,
|
|
50
|
-
specifier: portable.startsWith('.') ? portable : `./${portable}`,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function runtimeEntrySource(apiSpecifier, runtimeEntrypoints, entrypointPrefix) {
|
|
54
|
-
const prefix = parseProtocolQualifiedIdentifier(entrypointPrefix).split('.');
|
|
55
|
-
const names = JSON.stringify(runtimeEntrypoints);
|
|
56
|
-
let registryExpression = 'runtimeEntries';
|
|
57
|
-
for (let index = prefix.length - 1; index >= 1; index -= 1) {
|
|
58
|
-
registryExpression = `Object.freeze({ ${prefix[index]}: ${registryExpression} })`;
|
|
59
|
-
}
|
|
60
|
-
return [
|
|
61
|
-
`import * as apiModule from ${JSON.stringify(apiSpecifier)};`,
|
|
62
|
-
'const api = apiModule.api ?? Reflect.get(apiModule, "default");',
|
|
63
|
-
'if (!api || typeof api !== "object") throw new Error("Runtime API export is unavailable.");',
|
|
64
|
-
'const resolveQuery = (name) => {',
|
|
65
|
-
' if (typeof api.execute === "function") {',
|
|
66
|
-
' // Serve entrypoint names are validated against api.queries before bundling.',
|
|
67
|
-
' return (argument = {}) => api.execute(name, {',
|
|
68
|
-
' input: argument.input,',
|
|
69
|
-
' ...(argument.context === undefined ? {} : { context: argument.context }),',
|
|
70
|
-
' ...(argument.request === undefined ? {} : { request: argument.request }),',
|
|
71
|
-
' ...(argument.requestId === undefined ? {} : { requestId: argument.requestId }),',
|
|
72
|
-
// A null principal means "no trusted principal"; forwarding it would skip the
|
|
73
|
-
// API's own auth strategies instead of falling through to them.
|
|
74
|
-
' ...(argument.trustedAuth == null ? {} : { trustedAuth: argument.trustedAuth }),',
|
|
75
|
-
' });',
|
|
76
|
-
' }',
|
|
77
|
-
' const entry = api.queries?.[name];',
|
|
78
|
-
' const query = entry?.query;',
|
|
79
|
-
' let run;',
|
|
80
|
-
' if (typeof query === "function") run = query;',
|
|
81
|
-
' else if (query && typeof query === "object" && typeof query.run === "function") run = query.run.bind(query);',
|
|
82
|
-
' if (!run) throw new Error(`Runtime entrypoint ${name} is unavailable.`);',
|
|
83
|
-
' // Without a Serve execute() pipeline there is nothing to enforce roles, scopes,',
|
|
84
|
-
' // or tenancy, so refuse a trusted principal instead of silently ignoring it.',
|
|
85
|
-
' return (argument = {}) => {',
|
|
86
|
-
' if (argument?.trustedAuth != null) {',
|
|
87
|
-
' throw new Error(`Runtime entrypoint ${name} cannot enforce a trusted principal without a Serve execute() pipeline.`);',
|
|
88
|
-
' }',
|
|
89
|
-
' return run(argument);',
|
|
90
|
-
' };',
|
|
91
|
-
'};',
|
|
92
|
-
`const runtimeEntries = Object.freeze(Object.fromEntries(${names}.map(name => [name, resolveQuery(name)])));`,
|
|
93
|
-
`export const ${prefix[0]} = ${registryExpression};`,
|
|
94
|
-
'',
|
|
95
|
-
].join('\n');
|
|
96
|
-
}
|
|
97
|
-
export async function buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, entrypointPrefix = 'queries') {
|
|
98
|
-
if (runtimeEntrypoints.length === 0) {
|
|
99
|
-
throw new Error('Cannot build a runtime artifact without Serve query entrypoints.');
|
|
100
|
-
}
|
|
101
|
-
const names = getValidatedEntrypointNames(runtimeEntrypoints);
|
|
102
|
-
const { resolved, specifier } = projectRelativeImport(apiPath);
|
|
103
|
-
let source;
|
|
104
|
-
try {
|
|
105
|
-
source = runtimeEntrySource(specifier, names, entrypointPrefix);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
throw new Error(`Invalid runtime entrypoint prefix: ${entrypointPrefix}`);
|
|
109
|
-
}
|
|
110
|
-
try {
|
|
111
|
-
const result = await build({
|
|
112
|
-
stdin: {
|
|
113
|
-
contents: source,
|
|
114
|
-
loader: 'ts',
|
|
115
|
-
resolveDir: process.cwd(),
|
|
116
|
-
sourcefile: 'hypequery-runtime-entry.ts',
|
|
117
|
-
},
|
|
118
|
-
absWorkingDir: process.cwd(),
|
|
119
|
-
bundle: true,
|
|
120
|
-
charset: 'utf8',
|
|
121
|
-
format: 'esm',
|
|
122
|
-
legalComments: 'none',
|
|
123
|
-
logLevel: 'warning',
|
|
124
|
-
minify: true,
|
|
125
|
-
platform: 'node',
|
|
126
|
-
sourcemap: false,
|
|
127
|
-
target: ['node18'],
|
|
128
|
-
treeShaking: true,
|
|
129
|
-
tsconfig: await findNearestTsconfig(resolved) ?? undefined,
|
|
130
|
-
write: false,
|
|
131
|
-
});
|
|
132
|
-
const output = result.outputFiles?.find(file => file.path.endsWith('.js'))
|
|
133
|
-
?? result.outputFiles?.[0];
|
|
134
|
-
if (!output)
|
|
135
|
-
throw new Error('esbuild produced no output');
|
|
136
|
-
const bytes = output.contents;
|
|
137
|
-
return Object.freeze({
|
|
138
|
-
bytes,
|
|
139
|
-
artifactSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
140
|
-
entrypointPrefix,
|
|
141
|
-
runtimeEntrypoints: names,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
throw new Error(`Failed to build the Node runtime artifact from ${apiPath}.\n`
|
|
146
|
-
+ `Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function getValidatedEntrypointNames(input) {
|
|
150
|
-
const names = input.map((name) => {
|
|
151
|
-
try {
|
|
152
|
-
return parseProtocolIdentifier(name);
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
throw new Error(`Invalid runtime entrypoint name: ${name}`);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
if (new Set(names).size !== names.length) {
|
|
159
|
-
throw new Error('Runtime entrypoint names must be unique.');
|
|
160
|
-
}
|
|
161
|
-
return Object.freeze([...names].sort());
|
|
162
|
-
}
|