@commercetools/cli-topic-integration-layer 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/commands/integration-layer/explore.d.ts +20 -0
- package/dist/commands/integration-layer/explore.js +209 -0
- package/dist/commands/integration-layer/explore.js.map +1 -0
- package/dist/commands/integration-layer/extension/build.d.ts +2 -0
- package/dist/commands/integration-layer/extension/build.js +24 -3
- package/dist/commands/integration-layer/extension/build.js.map +1 -1
- package/dist/commands/integration-layer/extension/invoke.d.ts +1 -0
- package/dist/commands/integration-layer/extension/invoke.js +9 -4
- package/dist/commands/integration-layer/extension/invoke.js.map +1 -1
- package/dist/commands/integration-layer/extension/push.d.ts +15 -0
- package/dist/commands/integration-layer/extension/push.js +137 -3
- package/dist/commands/integration-layer/extension/push.js.map +1 -1
- package/dist/commands/integration-layer/extension/serve.d.ts +30 -0
- package/dist/commands/integration-layer/extension/serve.js +244 -6
- package/dist/commands/integration-layer/extension/serve.js.map +1 -1
- package/dist/commands/integration-layer/extension/status.js +4 -0
- package/dist/commands/integration-layer/extension/status.js.map +1 -1
- package/dist/commands/integration-layer/extension/validate.d.ts +2 -0
- package/dist/commands/integration-layer/extension/validate.js +24 -2
- package/dist/commands/integration-layer/extension/validate.js.map +1 -1
- package/dist/commands/integration-layer/init.js +228 -77
- package/dist/commands/integration-layer/init.js.map +1 -1
- package/dist/lib/awaitBundleState.d.ts +33 -0
- package/dist/lib/awaitBundleState.js +77 -0
- package/dist/lib/awaitBundleState.js.map +1 -0
- package/dist/lib/base.d.ts +35 -0
- package/dist/lib/base.js +99 -13
- package/dist/lib/base.js.map +1 -1
- package/dist/lib/ilClient.d.ts +62 -2
- package/dist/lib/ilClient.js +42 -2
- package/dist/lib/ilClient.js.map +1 -1
- package/dist/lib/sourceRevision.d.ts +39 -0
- package/dist/lib/sourceRevision.js +95 -0
- package/dist/lib/sourceRevision.js.map +1 -0
- package/dist/lib/tooling/exploreSchema.d.ts +15 -0
- package/dist/lib/tooling/exploreSchema.js +77 -0
- package/dist/lib/tooling/exploreSchema.js.map +1 -0
- package/dist/lib/tooling/explorerPage.d.ts +1 -0
- package/dist/lib/tooling/explorerPage.js +96 -0
- package/dist/lib/tooling/explorerPage.js.map +1 -0
- package/dist/lib/tooling/explorerServer.d.ts +27 -0
- package/dist/lib/tooling/explorerServer.js +134 -0
- package/dist/lib/tooling/explorerServer.js.map +1 -0
- package/dist/lib/tooling/extensions.d.ts +77 -0
- package/dist/lib/tooling/extensions.js +209 -0
- package/dist/lib/tooling/extensions.js.map +1 -0
- package/dist/lib/tooling/introspectionRequest.d.ts +13 -0
- package/dist/lib/tooling/introspectionRequest.js +47 -0
- package/dist/lib/tooling/introspectionRequest.js.map +1 -0
- package/dist/lib/tooling/session.d.ts +36 -0
- package/dist/lib/tooling/session.js +99 -0
- package/dist/lib/tooling/session.js.map +1 -0
- package/package.json +10 -12
|
@@ -1,105 +1,252 @@
|
|
|
1
1
|
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
3
|
import { Args, Command, Flags } from "@oclif/core";
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// // Example API Extension (uncomment to block a cart line item by SKU):
|
|
28
|
-
// export const apiExtensions = [
|
|
29
|
-
// {
|
|
30
|
-
// key: "block-sku",
|
|
31
|
-
// resourceTypeId: "cart",
|
|
32
|
-
// actions: ["Create", "Update"],
|
|
33
|
-
// handler: (input, ctx) => {
|
|
34
|
-
// const blocked = ctx.config.BLOCKED_SKU ?? "BLOCKED-SKU";
|
|
35
|
-
// const items = input.resource.obj?.lineItems ?? [];
|
|
36
|
-
// if (items.some((li) => li.variant?.sku === blocked)) {
|
|
37
|
-
// return { errors: [{ code: "SkuBlocked", message: \\\`\\\${blocked} is not purchasable\\\` }] };
|
|
38
|
-
// }
|
|
39
|
-
// return {};
|
|
40
|
-
// },
|
|
41
|
-
// },
|
|
42
|
-
// ];
|
|
43
|
-
`,
|
|
44
|
-
"package.json": `{
|
|
45
|
-
"name": "integration-layer-extension",
|
|
46
|
-
"version": "0.0.0",
|
|
4
|
+
// `init` scaffolds a MONOREPO you own and grow — a pnpm workspace whose members are
|
|
5
|
+
// individual extensions (`extensions/*`), seeded with one buildable hello-world
|
|
6
|
+
// extension that extends `Query`. The build/validate/push flow itself lives in this
|
|
7
|
+
// published CLI plugin, not in the scaffold, so an extension package stays tiny (just
|
|
8
|
+
// `src/extension.ts` + a thin package.json/tsconfig). Everything is vendored inline —
|
|
9
|
+
// no network fetch — and kept dependency-light so the extension commands work on it
|
|
10
|
+
// out of the box. The `basic` template is the only one today.
|
|
11
|
+
/** Derive a valid npm package name from the scaffold directory (fallback: a generic one). */
|
|
12
|
+
function projectNameFor(dir) {
|
|
13
|
+
const sanitized = basename(resolve(dir))
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
16
|
+
.replace(/^-+|-+$/g, "");
|
|
17
|
+
return sanitized || "integration-layer-extensions";
|
|
18
|
+
}
|
|
19
|
+
/** The monorepo file set, parameterized by the root package name. */
|
|
20
|
+
function templateFiles(projectName) {
|
|
21
|
+
return {
|
|
22
|
+
"package.json": `{
|
|
23
|
+
"name": ${JSON.stringify(projectName)},
|
|
47
24
|
"private": true,
|
|
48
25
|
"type": "module",
|
|
26
|
+
"packageManager": "pnpm@11.17.0",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": "22.x || 24.x"
|
|
29
|
+
},
|
|
49
30
|
"scripts": {
|
|
50
|
-
"build": "commercetools integration-layer extension build",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
31
|
+
"build": "commercetools integration-layer extension build --all",
|
|
32
|
+
"typecheck": "pnpm -r typecheck",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"dev": "commercetools integration-layer extension serve --all",
|
|
35
|
+
"validate": "commercetools integration-layer extension validate --all",
|
|
36
|
+
"push": "commercetools integration-layer extension push --all"
|
|
53
37
|
},
|
|
54
38
|
"devDependencies": {
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"typescript": "^
|
|
39
|
+
"eslint": "^10.8.0",
|
|
40
|
+
"typescript": "~6.0.3",
|
|
41
|
+
"typescript-eslint": "^8.65.0"
|
|
58
42
|
}
|
|
59
43
|
}
|
|
60
44
|
`,
|
|
61
|
-
|
|
45
|
+
"pnpm-workspace.yaml": `packages:
|
|
46
|
+
# Each extension is a workspace member (edit its src/extension.ts). A project deploys
|
|
47
|
+
# ONE bundle, so the root \`pnpm build\` / \`validate\` / \`push\` scripts MERGE every
|
|
48
|
+
# extension here into that single combined bundle — never one per package.
|
|
49
|
+
- 'extensions/*'
|
|
50
|
+
|
|
51
|
+
# Refuse dependency versions published less than 24h ago. Minutes; 1440 = 24h.
|
|
52
|
+
minimumReleaseAge: 1440
|
|
53
|
+
`,
|
|
54
|
+
"tsconfig.base.json": `{
|
|
62
55
|
"compilerOptions": {
|
|
63
56
|
"target": "ES2022",
|
|
64
57
|
"module": "NodeNext",
|
|
65
58
|
"moduleResolution": "NodeNext",
|
|
66
59
|
"strict": true,
|
|
60
|
+
"esModuleInterop": true,
|
|
67
61
|
"skipLibCheck": true,
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
"forceConsistentCasingInFileNames": true,
|
|
63
|
+
"resolveJsonModule": true
|
|
64
|
+
}
|
|
71
65
|
}
|
|
72
66
|
`,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
"eslint.config.js": `import tseslint from 'typescript-eslint';
|
|
68
|
+
|
|
69
|
+
export default tseslint.config(
|
|
70
|
+
{ ignores: ['**/dist/**', '**/node_modules/**'] },
|
|
71
|
+
...tseslint.configs.recommended,
|
|
72
|
+
{
|
|
73
|
+
// Allow \`_\`-prefixed unused args (resolver signatures often ignore arguments).
|
|
74
|
+
files: ['**/*.ts'],
|
|
75
|
+
rules: {
|
|
76
|
+
'@typescript-eslint/no-unused-vars': [
|
|
77
|
+
'error',
|
|
78
|
+
{
|
|
79
|
+
argsIgnorePattern: '^_',
|
|
80
|
+
varsIgnorePattern: '^_',
|
|
81
|
+
caughtErrorsIgnorePattern: '^_',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
);
|
|
77
87
|
`,
|
|
78
|
-
|
|
88
|
+
".gitignore": `node_modules/
|
|
79
89
|
dist/
|
|
80
90
|
.env
|
|
81
91
|
`,
|
|
82
|
-
|
|
92
|
+
".env.example": `# The authenticated commands (validate/push/status/delete, schema, config) reuse the
|
|
93
|
+
# token from \`commercetools auth login\` (as a manage_project client) — no credentials
|
|
94
|
+
# go here. Optionally override the integration-layer edge URL. Copy to .env.
|
|
95
|
+
INTEGRATION_LAYER_URL=
|
|
96
|
+
`,
|
|
97
|
+
"README.md": `# ${projectName}
|
|
98
|
+
|
|
99
|
+
A monorepo of commercetools integration-layer extensions, scaffolded by
|
|
100
|
+
\`commercetools integration-layer init\`. Each extension is a workspace member under
|
|
101
|
+
\`extensions/\`; the starter one (\`extensions/hello-world\`) adds a \`hello\` field to
|
|
102
|
+
\`Query\`.
|
|
103
|
+
|
|
104
|
+
A project deploys a **single bundle** — one federation subgraph the router composes
|
|
105
|
+
with the integration layer. So this repo's extensions are **merged into that one
|
|
106
|
+
bundle**: the root \`build\` / \`validate\` / \`push\` scripts combine everything under
|
|
107
|
+
\`extensions/\` and push it as one artifact (not one per package), and \`dev\` serves that
|
|
108
|
+
same merged subgraph locally. Split your code into as many \`extensions/*\` packages as
|
|
109
|
+
makes sense to author — they always ship as one.
|
|
110
|
+
|
|
111
|
+
## Set up the CLI (once)
|
|
112
|
+
|
|
113
|
+
The \`build\`/\`serve\`/\`validate\`/\`push\` scripts run through the **commercetools CLI**'s
|
|
114
|
+
\`integration-layer\` topic, added by the published
|
|
115
|
+
\`@commercetools/cli-topic-integration-layer\` plugin. Install the CLI globally with
|
|
116
|
+
pnpm, add the plugin, then log in:
|
|
117
|
+
|
|
118
|
+
\`\`\`sh
|
|
119
|
+
# Install the commercetools CLI globally with pnpm, then add the integration-layer
|
|
120
|
+
# topic. The plugin is on the public npm registry, so no auth or scope mapping is
|
|
121
|
+
# needed. (Install from @dev for now — the \`plugins\` command is only in the CLI's dev
|
|
122
|
+
# prerelease; drop @dev once it ships to @latest.)
|
|
123
|
+
pnpm add -g @commercetools/cli@dev
|
|
124
|
+
commercetools plugins install @commercetools/cli-topic-integration-layer
|
|
125
|
+
|
|
126
|
+
# Log in once — mints the manage_project token the authenticated commands reuse
|
|
127
|
+
# (validate/push/status/delete, schema, config) and sets your target project.
|
|
128
|
+
commercetools auth login --project-key <your-project-key>
|
|
129
|
+
\`\`\`
|
|
130
|
+
|
|
131
|
+
## Getting started
|
|
83
132
|
|
|
84
|
-
|
|
133
|
+
Everything runs through pnpm — the scripts below wrap the \`commercetools
|
|
134
|
+
integration-layer extension …\` commands.
|
|
85
135
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
- \`commercetools integration-layer extension push\` — validate + upload
|
|
136
|
+
\`\`\`sh
|
|
137
|
+
pnpm install
|
|
89
138
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
139
|
+
# From the repo root — these operate on the ONE combined bundle:
|
|
140
|
+
pnpm dev # merge every extension into one subgraph and serve it behind a local
|
|
141
|
+
# federated gateway (+ the integration layer) at
|
|
142
|
+
# http://localhost:4000/graphql — the production topology in miniature.
|
|
143
|
+
# Needs \`commercetools auth login\` (composes against your live schema).
|
|
144
|
+
pnpm validate # compose the combined bundle against the live schema (no upload)
|
|
145
|
+
pnpm push # validate + upload the combined bundle (replaces the project's one)
|
|
146
|
+
|
|
147
|
+
# …or focus on a single extension in isolation (standalone subgraph, no login needed):
|
|
148
|
+
pnpm --filter @extensions/hello-world dev
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
With \`pnpm dev\` running, open http://localhost:4000/graphql for a GraphiQL over the
|
|
152
|
+
merged schema; the raw combined extensions subgraph is at \`/_extension\`, and the
|
|
153
|
+
browsable merged (extensions + integration layer) schema at \`/composed\`.
|
|
154
|
+
|
|
155
|
+
Optionally copy \`.env.example\` to \`.env\` to override the integration-layer edge URL.
|
|
156
|
+
|
|
157
|
+
## Adding another extension
|
|
158
|
+
|
|
159
|
+
Copy \`extensions/hello-world\` to \`extensions/<your-extension>\`, rename it in its
|
|
160
|
+
\`package.json\`, and edit its \`src/extension.ts\`. The root scripts discover every folder
|
|
161
|
+
under \`extensions/\` automatically and merge it into the single bundle — so \`pnpm dev\`,
|
|
162
|
+
\`validate\`, and \`push\` all just pick it up, with no port wrangling. (Two extensions
|
|
163
|
+
can each add fields to \`Query\`; they only clash if they declare the *same* field.)
|
|
93
164
|
`,
|
|
165
|
+
"extensions/hello-world/package.json": `{
|
|
166
|
+
"name": "@extensions/hello-world",
|
|
167
|
+
"version": "0.1.0",
|
|
168
|
+
"private": true,
|
|
169
|
+
"type": "module",
|
|
170
|
+
"scripts": {
|
|
171
|
+
"build": "commercetools integration-layer extension build",
|
|
172
|
+
"dev": "commercetools integration-layer extension serve",
|
|
173
|
+
"typecheck": "tsc --noEmit"
|
|
174
|
+
},
|
|
175
|
+
"devDependencies": {
|
|
176
|
+
"@types/node": "^22.20.1",
|
|
177
|
+
"typescript": "~6.0.3"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
`,
|
|
181
|
+
"extensions/hello-world/tsconfig.json": `{
|
|
182
|
+
"extends": "../../tsconfig.base.json",
|
|
183
|
+
"compilerOptions": {
|
|
184
|
+
"noEmit": true,
|
|
185
|
+
"types": ["node"]
|
|
186
|
+
},
|
|
187
|
+
"include": ["src"],
|
|
188
|
+
"exclude": ["node_modules", "dist"]
|
|
189
|
+
}
|
|
190
|
+
`,
|
|
191
|
+
"extensions/hello-world/src/extension.ts": `/**
|
|
192
|
+
* A hello-world integration-layer extension — the PURELY ADDITIVE pattern: it adds a
|
|
193
|
+
* brand-new root field (\`Query.hello\`) and shares nothing with the integration layer,
|
|
194
|
+
* so the two \`Query\` types merge with no change to it and composition is trivial.
|
|
195
|
+
*
|
|
196
|
+
* An extension exports \`typeDefs\` (a federation-v2 SDL string) + \`resolvers\` and runs
|
|
197
|
+
* in a restricted runtime. Edit this file, then from the repo ROOT run \`pnpm validate\` /
|
|
198
|
+
* \`pnpm push\` — every extension is merged into the one bundle the project deploys (the
|
|
199
|
+
* target project comes from your \`commercetools auth login\`). Iterate on just this one
|
|
200
|
+
* with \`pnpm --filter @extensions/hello-world dev\`.
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
export const typeDefs = \`
|
|
204
|
+
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3")
|
|
205
|
+
|
|
206
|
+
type Query {
|
|
207
|
+
"A friendly greeting contributed by this extension."
|
|
208
|
+
hello(name: String): String!
|
|
209
|
+
}
|
|
210
|
+
\`;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The per-call capability context the restricted runtime hands each resolver.
|
|
214
|
+
* \`now()\` is the current epoch millis (a convenience — \`Date.now()\` works too);
|
|
215
|
+
* \`config\` is this extension's merchant-supplied \`{ key: value }\` map, with secret
|
|
216
|
+
* values decrypted host-side (e.g. \`ctx.config.API_KEY\`). Set values with
|
|
217
|
+
* \`commercetools integration-layer config set <KEY> <VALUE>\`.
|
|
218
|
+
*/
|
|
219
|
+
interface ExtensionContext {
|
|
220
|
+
now(): number;
|
|
221
|
+
config: Readonly<Record<string, string>>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const resolvers = {
|
|
225
|
+
Query: {
|
|
226
|
+
// A resolver receives (parent, args, context): \`_parent\` is the object the field
|
|
227
|
+
// hangs off (nothing here — this is a root field), \`args\` are the field arguments
|
|
228
|
+
// from the query, and \`ctx\` exposes the restricted runtime's host capabilities.
|
|
229
|
+
hello: (_parent: unknown, { name }: { name?: string }, ctx: ExtensionContext) => {
|
|
230
|
+
const greeting = ctx.config.GREETING ?? "Hello";
|
|
231
|
+
return \`\${greeting}, \${name ?? "world"}, from your integration-layer extension!\`;
|
|
232
|
+
},
|
|
233
|
+
},
|
|
94
234
|
};
|
|
235
|
+
`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
95
238
|
export default class Init extends Command {
|
|
96
|
-
static description = "Scaffold an integration-layer
|
|
239
|
+
static description = "Scaffold an integration-layer extensions monorepo (a pnpm workspace with one hello-world extension)";
|
|
97
240
|
static examples = [
|
|
98
|
-
"<%= config.bin %> integration-layer init
|
|
99
|
-
"<%= config.bin %> integration-layer init my-
|
|
241
|
+
"<%= config.bin %> integration-layer init",
|
|
242
|
+
"<%= config.bin %> integration-layer init my-extensions",
|
|
243
|
+
"<%= config.bin %> integration-layer init my-extensions --template basic",
|
|
100
244
|
];
|
|
101
245
|
static args = {
|
|
102
|
-
directory: Args.string({
|
|
246
|
+
directory: Args.string({
|
|
247
|
+
description: "directory to scaffold the monorepo into (defaults to the current directory)",
|
|
248
|
+
default: ".",
|
|
249
|
+
}),
|
|
103
250
|
};
|
|
104
251
|
static flags = {
|
|
105
252
|
template: Flags.string({
|
|
@@ -118,18 +265,22 @@ export default class Init extends Command {
|
|
|
118
265
|
const dir = args.directory;
|
|
119
266
|
const existing = await readdir(dir).catch(() => []);
|
|
120
267
|
if (existing.length > 0 && !flags.force) {
|
|
121
|
-
|
|
268
|
+
const what = dir === "." ? "The current directory" : `Directory '${dir}'`;
|
|
269
|
+
this.error(`${what} is not empty. Pass --force to scaffold into it anyway.`);
|
|
122
270
|
}
|
|
123
|
-
|
|
271
|
+
const files = templateFiles(projectNameFor(dir));
|
|
272
|
+
for (const [relPath, contents] of Object.entries(files)) {
|
|
124
273
|
const target = join(dir, relPath);
|
|
125
274
|
await mkdir(join(target, ".."), { recursive: true });
|
|
126
275
|
await writeFile(target, contents, "utf8");
|
|
127
276
|
}
|
|
128
|
-
|
|
277
|
+
const location = dir === "." ? "the current directory" : dir;
|
|
278
|
+
this.log(`✓ scaffolded a '${flags.template}' extensions monorepo in ${location}`);
|
|
129
279
|
this.log("Next steps:");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.log("
|
|
280
|
+
if (dir !== ".")
|
|
281
|
+
this.log(` cd ${dir}`);
|
|
282
|
+
this.log(" pnpm install");
|
|
283
|
+
this.log(" pnpm dev # run the hello-world extension locally");
|
|
133
284
|
}
|
|
134
285
|
}
|
|
135
286
|
//# sourceMappingURL=init.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/integration-layer/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/integration-layer/init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ExtensionMeta } from "./ilClient.js";
|
|
2
|
+
/** How the wait ended. Only `failed` means "the push produced a broken bundle". */
|
|
3
|
+
export type BundleOutcome = {
|
|
4
|
+
kind: "running";
|
|
5
|
+
meta: ExtensionMeta;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "failed";
|
|
8
|
+
meta: ExtensionMeta;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "superseded";
|
|
11
|
+
meta: ExtensionMeta;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "unknown";
|
|
14
|
+
reason: string;
|
|
15
|
+
meta?: ExtensionMeta;
|
|
16
|
+
};
|
|
17
|
+
export interface AwaitOptions {
|
|
18
|
+
/** Give up after this long. */
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
/** Gap between metadata reads. */
|
|
21
|
+
intervalMs: number;
|
|
22
|
+
/** Injected so tests don't actually sleep. */
|
|
23
|
+
sleep?: (ms: number) => Promise<void>;
|
|
24
|
+
/** Injected so tests control elapsed time. */
|
|
25
|
+
now?: () => number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Poll until `version` leaves `pending`, or we run out of time.
|
|
29
|
+
*
|
|
30
|
+
* The first read happens immediately — an extension that was already mid-poll can
|
|
31
|
+
* settle the version before we ever sleep.
|
|
32
|
+
*/
|
|
33
|
+
export declare function awaitBundleState(baseUrl: string, projectKey: string, token: string, version: number, options: AwaitOptions): Promise<BundleOutcome>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Wait for a just-pushed bundle version to prove itself.
|
|
2
|
+
//
|
|
3
|
+
// A push only STORES a bundle. The extension picks it up on its own poll, tries to
|
|
4
|
+
// load it, and reports back — so the version starts `pending` and only becomes
|
|
5
|
+
// `running` (it loaded and published its schema) or `failed` (it didn't, with a
|
|
6
|
+
// reason) a little later. Until this, `push` reported success the moment the bytes
|
|
7
|
+
// landed, which meant a bundle the sandbox refuses to run looked like a clean push.
|
|
8
|
+
//
|
|
9
|
+
// This polls the project's bundle metadata until the pushed version settles, and
|
|
10
|
+
// hands the caller back an outcome it can turn into an exit code.
|
|
11
|
+
//
|
|
12
|
+
// Three things it deliberately does NOT do:
|
|
13
|
+
//
|
|
14
|
+
// - It never treats "I couldn't find out" as a failure. An integration layer that
|
|
15
|
+
// predates bundle state reports no `state` at all, and a project whose extension
|
|
16
|
+
// isn't deployed yet will never move off `pending`. Both are `unknown`, not
|
|
17
|
+
// `failed` — failing a push for them would break perfectly good workflows.
|
|
18
|
+
// - It doesn't watch for the version to change under it. If someone else pushes
|
|
19
|
+
// while we wait, the newest version is no longer ours; we say so and stop rather
|
|
20
|
+
// than report on somebody else's push.
|
|
21
|
+
// - It doesn't retry transient metadata-read errors into a verdict. A read that
|
|
22
|
+
// throws ends the wait as `unknown` with the error attached.
|
|
23
|
+
import { fetchExtensionMeta } from "./ilClient.js";
|
|
24
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
/**
|
|
26
|
+
* Poll until `version` leaves `pending`, or we run out of time.
|
|
27
|
+
*
|
|
28
|
+
* The first read happens immediately — an extension that was already mid-poll can
|
|
29
|
+
* settle the version before we ever sleep.
|
|
30
|
+
*/
|
|
31
|
+
export async function awaitBundleState(baseUrl, projectKey, token, version, options) {
|
|
32
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
33
|
+
const now = options.now ?? Date.now;
|
|
34
|
+
const deadline = now() + options.timeoutMs;
|
|
35
|
+
for (;;) {
|
|
36
|
+
let meta;
|
|
37
|
+
try {
|
|
38
|
+
meta = await fetchExtensionMeta(baseUrl, projectKey, token);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return {
|
|
42
|
+
kind: "unknown",
|
|
43
|
+
reason: `could not read the stored bundle's status: ${err.message}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (meta === null) {
|
|
47
|
+
// Deleted out from under us between the push and this read.
|
|
48
|
+
return { kind: "unknown", reason: "no bundle is stored for this project any more" };
|
|
49
|
+
}
|
|
50
|
+
if (meta.state === undefined) {
|
|
51
|
+
// An integration layer that doesn't report state. Nothing to wait for.
|
|
52
|
+
return {
|
|
53
|
+
kind: "unknown",
|
|
54
|
+
reason: "this integration layer does not report bundle status",
|
|
55
|
+
meta,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (meta.version !== version) {
|
|
59
|
+
// Someone else pushed while we were waiting; the newest version isn't ours.
|
|
60
|
+
return { kind: "superseded", meta };
|
|
61
|
+
}
|
|
62
|
+
if (meta.state === "running")
|
|
63
|
+
return { kind: "running", meta };
|
|
64
|
+
if (meta.state === "failed")
|
|
65
|
+
return { kind: "failed", meta };
|
|
66
|
+
// Still pending.
|
|
67
|
+
if (now() >= deadline) {
|
|
68
|
+
return {
|
|
69
|
+
kind: "unknown",
|
|
70
|
+
reason: `the extension did not report on version ${version} within ${Math.round(options.timeoutMs / 1000)}s`,
|
|
71
|
+
meta,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
await sleep(options.intervalMs);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=awaitBundleState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"awaitBundleState.js","sourceRoot":"","sources":["../../src/lib/awaitBundleState.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,EAAE;AACF,mFAAmF;AACnF,+EAA+E;AAC/E,gFAAgF;AAChF,mFAAmF;AACnF,oFAAoF;AACpF,EAAE;AACF,iFAAiF;AACjF,kEAAkE;AAClE,EAAE;AACF,4CAA4C;AAC5C,EAAE;AACF,oFAAoF;AACpF,qFAAqF;AACrF,gFAAgF;AAChF,+EAA+E;AAC/E,kFAAkF;AAClF,qFAAqF;AACrF,2CAA2C;AAC3C,kFAAkF;AAClF,iEAAiE;AAEjE,OAAO,EAAE,kBAAkB,EAAsB,MAAM,eAAe,CAAC;AAoBvE,MAAM,YAAY,GAAG,CAAC,EAAU,EAAiB,EAAE,CACjD,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAe,EACf,UAAkB,EAClB,KAAa,EACb,OAAe,EACf,OAAqB;IAErB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACpC,MAAM,QAAQ,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IAE3C,SAAS,CAAC;QACR,IAAI,IAA0B,CAAC;QAC/B,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,kBAAkB,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;QAC9D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,IAAI,EAAE,SAAS;gBACf,MAAM,EAAE,8CAA+C,GAAa,CAAC,OAAO,EAAE;aAC/E,CAAC;QACJ,CAAC;QAED,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,4DAA4D;YAC5D,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,+CAA+C,EAAE,CAAC;QACtF,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,uEAAuE;YACvE,OAAO;gBACL,IAAI,EAAE,SAAS;gBACf,MAAM,EAAE,sDAAsD;gBAC9D,IAAI;aACL,CAAC;QACJ,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YAC7B,4EAA4E;YAC5E,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QACtC,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QAC/D,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAE7D,iBAAiB;QACjB,IAAI,GAAG,EAAE,IAAI,QAAQ,EAAE,CAAC;YACtB,OAAO;gBACL,IAAI,EAAE,SAAS;gBACf,MAAM,EAAE,2CAA2C,OAAO,WAAW,IAAI,CAAC,KAAK,CAC7E,OAAO,CAAC,SAAS,GAAG,IAAI,CACzB,GAAG;gBACJ,IAAI;aACL,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC;AACH,CAAC"}
|
package/dist/lib/base.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { AuthCommand, CtpAuthorizedClient } from "@commercetools/cli-plugin-auth";
|
|
2
|
+
/**
|
|
3
|
+
* Reconstruct the logged-in principal from the persisted credentials file. Returns
|
|
4
|
+
* undefined when no valid client-credentials login is on disk (absent file, or a
|
|
5
|
+
* different auth kind), so unauthorized commands still run. No network. Exported for
|
|
6
|
+
* tests; commands go through {@link IntegrationLayerCommand.requirePrincipal}.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadPersistedPrincipal(credentialsFile?: string): Promise<CtpAuthorizedClient | undefined>;
|
|
2
9
|
/** The resolved, ready-to-use integration-layer context: where to call and as whom. */
|
|
3
10
|
export interface IlContext {
|
|
4
11
|
/** The integration-layer edge base URL, trailing slashes trimmed. */
|
|
@@ -26,6 +33,21 @@ export interface IlFlagValues {
|
|
|
26
33
|
* only when the region is absent, so resolveIlContext then fails loudly.
|
|
27
34
|
*/
|
|
28
35
|
export declare function edgeUrlForRegion(region: string): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The GRAPHQL edge (the integration-router) for a region — where operations are
|
|
38
|
+
* actually served, at `/{project}/graphql`. A distinct host from the extensions
|
|
39
|
+
* edge above: the extensions edge is the connector backend (manage_project routes),
|
|
40
|
+
* the graphql edge is the router. `--graphql-url` / `IL_GRAPHQL_URL` overrides it
|
|
41
|
+
* for staging zones, which don't follow the production convention.
|
|
42
|
+
*/
|
|
43
|
+
export declare function graphqlEdgeUrlForRegion(region: string): string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* The IDENTITY edge (the storefront pod) for a region — where sessions are minted,
|
|
46
|
+
* at `POST /{project}/session`. Again a distinct host from both of the above; the
|
|
47
|
+
* four-pod split put shopper identity, shopper GraphQL, and the machine surface on
|
|
48
|
+
* separate ingresses. `--auth-url` / `IL_AUTH_URL` overrides it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function authEdgeUrlForRegion(region: string): string | undefined;
|
|
29
51
|
export declare abstract class IntegrationLayerCommand extends AuthCommand {
|
|
30
52
|
/**
|
|
31
53
|
* Whether the command needs a logged-in principal. True for every command that
|
|
@@ -34,11 +56,24 @@ export declare abstract class IntegrationLayerCommand extends AuthCommand {
|
|
|
34
56
|
* to fail loudly if the network path is actually taken.
|
|
35
57
|
*/
|
|
36
58
|
protected authorized: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The principal loaded from `~/.commercetools/credentials` in {@link init}, or
|
|
61
|
+
* undefined when no valid login is persisted. Resolved once per command run so the
|
|
62
|
+
* synchronous {@link requirePrincipal} call sites (here and in `explore`) are unchanged.
|
|
63
|
+
*/
|
|
64
|
+
private persistedPrincipal?;
|
|
37
65
|
static baseFlags: {
|
|
38
66
|
"integration-layer-url": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
39
67
|
"project-key": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
40
68
|
};
|
|
41
69
|
protected init(): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Read the persisted login from `~/.commercetools/credentials`. Returns undefined when
|
|
72
|
+
* no login is on disk (so unauthorized commands like standalone `serve` still run) and
|
|
73
|
+
* only fails loudly when the file is present but malformed. No network — the token was
|
|
74
|
+
* already minted by `auth login` and serialized whole.
|
|
75
|
+
*/
|
|
76
|
+
private loadPersistedPrincipal;
|
|
42
77
|
/** The logged-in commercetools principal, or an `AuthorizationError` pointing at `auth login`. */
|
|
43
78
|
protected requirePrincipal(): CtpAuthorizedClient;
|
|
44
79
|
/**
|