@vendure-platform/create 0.5.1 → 1.0.0-major.202609091053.0.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 +48 -36
- package/dist/add-to-existing.d.ts +19 -13
- package/dist/add-to-existing.js +1 -1
- package/dist/assets/demo-data/assets/branding/logo.svg +4 -0
- package/dist/assets/demo-data/assets/images/absorbent-pad.webp +0 -0
- package/dist/assets/demo-data/assets/images/adjustable-wrench.webp +0 -0
- package/dist/assets/demo-data/assets/images/anchor-bolt.webp +0 -0
- package/dist/assets/demo-data/assets/images/angle-grinder.webp +0 -0
- package/dist/assets/demo-data/assets/images/barrier-tape.webp +0 -0
- package/dist/assets/demo-data/assets/images/bench-grinder.webp +0 -0
- package/dist/assets/demo-data/assets/images/bracket.webp +0 -0
- package/dist/assets/demo-data/assets/images/broom.webp +0 -0
- package/dist/assets/demo-data/assets/images/bucket.webp +0 -0
- package/dist/assets/demo-data/assets/images/cable-gland.webp +0 -0
- package/dist/assets/demo-data/assets/images/cable-tie.webp +0 -0
- package/dist/assets/demo-data/assets/images/cable-tray.webp +0 -0
- package/dist/assets/demo-data/assets/images/cable.webp +0 -0
- package/dist/assets/demo-data/assets/images/chimney-base-kit.webp +0 -0
- package/dist/assets/demo-data/assets/images/chimney-cowl-bird-guard.webp +0 -0
- package/dist/assets/demo-data/assets/images/chimney-cowl-open.webp +0 -0
- package/dist/assets/demo-data/assets/images/chimney-storm-collar.webp +0 -0
- package/dist/assets/demo-data/assets/images/chimney-wall-bracket.webp +0 -0
- package/dist/assets/demo-data/assets/images/circular-saw.webp +0 -0
- package/dist/assets/demo-data/assets/images/cleaning-spray.webp +0 -0
- package/dist/assets/demo-data/assets/images/conduit.webp +0 -0
- package/dist/assets/demo-data/assets/images/cordless-drill.webp +0 -0
- package/dist/assets/demo-data/assets/images/drill-bits.webp +0 -0
- package/dist/assets/demo-data/assets/images/dust-mask.webp +0 -0
- package/dist/assets/demo-data/assets/images/ear-defenders.webp +0 -0
- package/dist/assets/demo-data/assets/images/ear-plugs.webp +0 -0
- package/dist/assets/demo-data/assets/images/extension-lead.webp +0 -0
- package/dist/assets/demo-data/assets/images/eye-wash-station.webp +0 -0
- package/dist/assets/demo-data/assets/images/face-shield.webp +0 -0
- package/dist/assets/demo-data/assets/images/file-set.webp +0 -0
- package/dist/assets/demo-data/assets/images/first-aid-kit.webp +0 -0
- package/dist/assets/demo-data/assets/images/floor-sign.webp +0 -0
- package/dist/assets/demo-data/assets/images/flue-bend-45.webp +0 -0
- package/dist/assets/demo-data/assets/images/flue-bend-90.webp +0 -0
- package/dist/assets/demo-data/assets/images/flue-section-1000mm.webp +0 -0
- package/dist/assets/demo-data/assets/images/flue-section-250mm.webp +0 -0
- package/dist/assets/demo-data/assets/images/flue-section-500mm.webp +0 -0
- package/dist/assets/demo-data/assets/images/fuse.webp +0 -0
- package/dist/assets/demo-data/assets/images/glove-dispenser.webp +0 -0
- package/dist/assets/demo-data/assets/images/grinding-disc.webp +0 -0
- package/dist/assets/demo-data/assets/images/hacksaw.webp +0 -0
- package/dist/assets/demo-data/assets/images/hammer.webp +0 -0
- package/dist/assets/demo-data/assets/images/hand-sanitiser.webp +0 -0
- package/dist/assets/demo-data/assets/images/head-torch.webp +0 -0
- package/dist/assets/demo-data/assets/images/heat-gun.webp +0 -0
- package/dist/assets/demo-data/assets/images/hex-bolt.webp +0 -0
- package/dist/assets/demo-data/assets/images/hex-key-set.webp +0 -0
- package/dist/assets/demo-data/assets/images/hex-nut.webp +0 -0
- package/dist/assets/demo-data/assets/images/hi-vis-jacket.webp +0 -0
- package/dist/assets/demo-data/assets/images/hi-vis-vest.webp +0 -0
- package/dist/assets/demo-data/assets/images/hose-clamp.webp +0 -0
- package/dist/assets/demo-data/assets/images/impact-driver.webp +0 -0
- package/dist/assets/demo-data/assets/images/jigsaw.webp +0 -0
- package/dist/assets/demo-data/assets/images/junction-box.webp +0 -0
- package/dist/assets/demo-data/assets/images/knee-pads.webp +0 -0
- package/dist/assets/demo-data/assets/images/machine-screw.webp +0 -0
- package/dist/assets/demo-data/assets/images/made-to-order-configurable-chimney.webp +0 -0
- package/dist/assets/demo-data/assets/images/mallet.webp +0 -0
- package/dist/assets/demo-data/assets/images/mcb.webp +0 -0
- package/dist/assets/demo-data/assets/images/mop.webp +0 -0
- package/dist/assets/demo-data/assets/images/paper-towels.webp +0 -0
- package/dist/assets/demo-data/assets/images/pipe-wrench.webp +0 -0
- package/dist/assets/demo-data/assets/images/pliers-set.webp +0 -0
- package/dist/assets/demo-data/assets/images/pry-bar.webp +0 -0
- package/dist/assets/demo-data/assets/images/rcd.webp +0 -0
- package/dist/assets/demo-data/assets/images/reciprocating-saw.webp +0 -0
- package/dist/assets/demo-data/assets/images/rivet.webp +0 -0
- package/dist/assets/demo-data/assets/images/roof-flashing-high-pitch.webp +0 -0
- package/dist/assets/demo-data/assets/images/roof-flashing-low-pitch.webp +0 -0
- package/dist/assets/demo-data/assets/images/safety-boots.webp +0 -0
- package/dist/assets/demo-data/assets/images/safety-glasses.webp +0 -0
- package/dist/assets/demo-data/assets/images/safety-harness.webp +0 -0
- package/dist/assets/demo-data/assets/images/safety-helmet.webp +0 -0
- package/dist/assets/demo-data/assets/images/safety-sign.webp +0 -0
- package/dist/assets/demo-data/assets/images/sanding-disc.webp +0 -0
- package/dist/assets/demo-data/assets/images/saw-blade.webp +0 -0
- package/dist/assets/demo-data/assets/images/screwdriver-set.webp +0 -0
- package/dist/assets/demo-data/assets/images/screwdriver.webp +0 -0
- package/dist/assets/demo-data/assets/images/self-tapping-screw.webp +0 -0
- package/dist/assets/demo-data/assets/images/socket-set.webp +0 -0
- package/dist/assets/demo-data/assets/images/socket.webp +0 -0
- package/dist/assets/demo-data/assets/images/spanner-set.webp +0 -0
- package/dist/assets/demo-data/assets/images/spanner.webp +0 -0
- package/dist/assets/demo-data/assets/images/spill-kit.webp +0 -0
- package/dist/assets/demo-data/assets/images/spirit-level.webp +0 -0
- package/dist/assets/demo-data/assets/images/switch.webp +0 -0
- package/dist/assets/demo-data/assets/images/tape-measure.webp +0 -0
- package/dist/assets/demo-data/assets/images/terminal-block.webp +0 -0
- package/dist/assets/demo-data/assets/images/threaded-rod.webp +0 -0
- package/dist/assets/demo-data/assets/images/torque-wrench.webp +0 -0
- package/dist/assets/demo-data/assets/images/utility-knife.webp +0 -0
- package/dist/assets/demo-data/assets/images/wall-pass-sleeve.webp +0 -0
- package/dist/assets/demo-data/assets/images/wall-plug.webp +0 -0
- package/dist/assets/demo-data/assets/images/washer.webp +0 -0
- package/dist/assets/demo-data/assets/images/waste-bin.webp +0 -0
- package/dist/assets/demo-data/assets/images/wire-connector.webp +0 -0
- package/dist/assets/demo-data/assets/images/wire-cutters.webp +0 -0
- package/dist/assets/demo-data/assets/images/wood-screw.webp +0 -0
- package/dist/assets/demo-data/assets/images/work-gloves.webp +0 -0
- package/dist/assets/demo-data/assets/images/work-light.webp +0 -0
- package/dist/assets/demo-data/assets/initial-data.json +667 -0
- package/dist/assets/demo-data/assets/products.csv +779 -0
- package/dist/assets/storefront/next-env.d.ts +2 -1
- package/dist/assets/storefront/next.config.ts +14 -13
- package/dist/assets/storefront/package.json +3 -2
- package/dist/assets/storefront/src/app/[locale]/account/addresses/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/account/approvals/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/account/company/business-units/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/account/company/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/account/company/payment-methods/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/account/company/roles/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/account/company/users/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/account/profile/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/account/quick-order/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/account/quotes/[id]/page.tsx +10 -6
- package/dist/assets/storefront/src/app/[locale]/cart/order-summary.tsx +8 -5
- package/dist/assets/storefront/src/app/[locale]/cart/promotion-code.tsx +9 -6
- package/dist/assets/storefront/src/app/[locale]/checkout/page.tsx +2 -0
- package/dist/assets/storefront/src/app/[locale]/layout.tsx +3 -3
- package/dist/assets/storefront/src/app/[locale]/order-confirmation/[code]/order-confirmation.tsx +2 -60
- package/dist/assets/storefront/src/components/commerce/product-card-list.tsx +4 -2
- package/dist/assets/storefront/src/components/commerce/product-card.tsx +4 -2
- package/dist/assets/storefront/src/components/commerce/quote-action-buttons.tsx +31 -14
- package/dist/assets/storefront/src/config/capabilities.ts +41 -0
- package/dist/assets/storefront/src/config/scenario.ts +10 -0
- package/dist/assets/storefront/src/graphql-env.d.ts +47 -41
- package/dist/assets/storefront/src/i18n/server.ts +33 -5
- package/dist/assets/storefront/src/lib/backend-health.ts +11 -3
- package/dist/assets/storefront/src/lib/vendure/capability-documents.ts +246 -0
- package/dist/assets/storefront/src/lib/vendure/capability-schema.ts +358 -0
- package/dist/assets/storefront/src/lib/vendure/capability-ui.ts +15 -0
- package/dist/assets/storefront/src/lib/vendure/mutations.ts +835 -532
- package/dist/assets/storefront/src/lib/vendure/queries.ts +862 -519
- package/dist/assets/storefront/src/proxy.ts +11 -0
- package/dist/assets/storefront/src/schema.graphql +337 -243
- package/dist/assets/templates/README.md.hbs +25 -1
- package/dist/assets/templates/docker-compose.yml.hbs +4 -0
- package/dist/assets/templates/env.hbs +34 -9
- package/dist/assets/templates/environment.d.ts.hbs +1 -0
- package/dist/assets/templates/gitignore.hbs +7 -4
- package/dist/assets/templates/migrate.ts.hbs +55 -0
- package/dist/assets/templates/npmrc.hbs +7 -2
- package/dist/assets/templates/platform-edition.ts.hbs +21 -0
- package/dist/assets/templates/server-package.json.hbs +15 -8
- package/dist/assets/templates/vendure-config.ts.hbs +18 -0
- package/dist/bin/run.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/console-onboarding.d.ts +44 -0
- package/dist/console-onboarding.js +1 -0
- package/dist/hub-projects.d.ts +48 -0
- package/dist/hub-projects.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin-registry.d.ts +61 -2
- package/dist/plugin-registry.js +1 -1
- package/dist/registry.d.ts +16 -13
- package/dist/registry.js +1 -1
- package/dist/scaffold-shared.js +1 -1
- package/dist/scaffold-storefront-only.js +1 -1
- package/dist/scaffolder.d.ts +11 -0
- package/dist/scaffolder.js +1 -1
- package/dist/storefront-copier.d.ts +7 -2
- package/dist/storefront-copier.js +1 -1
- package/dist/template-renderer.d.ts +26 -5
- package/dist/template-renderer.js +1 -1
- package/dist/types.d.ts +54 -23
- package/package.json +14 -2
- package/dist/assets/storefront/src/middleware.ts +0 -11
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
import { hasLocale } from 'next-intl';
|
|
2
|
-
import { getLocale } from 'next-intl/server';
|
|
2
|
+
import { getLocale, setRequestLocale } from 'next-intl/server';
|
|
3
|
+
import { locale as rootLocale } from 'next/root-params';
|
|
3
4
|
import { routing, type Locale } from './routing';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Resolve the current locale from
|
|
7
|
+
* Resolve the current locale from the `[locale]` root param, with validation.
|
|
8
|
+
* Falls back to defaultLocale if the segment isn't in routing config.
|
|
9
|
+
*
|
|
10
|
+
* This is the default on the server, including 'use cache' scopes. Root params
|
|
11
|
+
* carry no request state, so a component that resolves the locale this way
|
|
12
|
+
* still prerenders.
|
|
13
|
+
*/
|
|
14
|
+
export async function getRootLocale(): Promise<Locale> {
|
|
15
|
+
const loc = await rootLocale();
|
|
16
|
+
return hasLocale(routing.locales, loc) ? (loc as Locale) : routing.defaultLocale;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the current locale from request context, with validation.
|
|
7
21
|
* Falls back to defaultLocale if the resolved locale isn't in routing config.
|
|
8
22
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
23
|
+
* Only for scopes root params cannot reach: Client Components, Server Actions
|
|
24
|
+
* and Route Handlers, plus server code that already renders dynamically.
|
|
25
|
+
* Reading request state blocks prerendering, so a Server Component that calls
|
|
26
|
+
* this serves no static shell — call `getRootLocale()` there instead.
|
|
12
27
|
*/
|
|
13
28
|
export async function getRouteLocale(): Promise<Locale> {
|
|
14
29
|
const loc = await getLocale();
|
|
15
30
|
return hasLocale(routing.locales, loc) ? (loc as Locale) : routing.defaultLocale;
|
|
16
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pin the `[locale]` root param as next-intl's request locale.
|
|
35
|
+
*
|
|
36
|
+
* Call this before the first next-intl call in a Server Component that does not
|
|
37
|
+
* already pin the locale from `params`. Without it, `getTranslations()` resolves
|
|
38
|
+
* the locale through next-intl's request scope, which under `cacheComponents` is
|
|
39
|
+
* uncached request state: the route then fails `next build --debug-prerender`
|
|
40
|
+
* and serves no static shell.
|
|
41
|
+
*/
|
|
42
|
+
export async function pinRootLocale(): Promise<void> {
|
|
43
|
+
setRequestLocale(await getRootLocale());
|
|
44
|
+
}
|
|
@@ -10,12 +10,20 @@ const HEALTH_CHECK_TIMEOUT_MS = 3000;
|
|
|
10
10
|
* on Vercel) but every API call fails, leaving the user with a branded but
|
|
11
11
|
* empty shell. When this returns false we render a "Demo Paused" page instead.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* This is cacheLife('seconds') with one field changed: `expire` goes from 60 to
|
|
14
|
+
* 300. The preset's 60s expire is too short for Next 16.3's build-time cache,
|
|
15
|
+
* which drops the entry between the cache-warming phase and the final prerender
|
|
16
|
+
* phase, and every prerendered page that mounts this gate then logs an
|
|
17
|
+
* "Unexpected cache miss" during `next build`. Measured on 16.3: expire 300 is
|
|
18
|
+
* retained, 120 is not.
|
|
19
|
+
*
|
|
20
|
+
* `stale` and `revalidate` keep the preset's values, so how fast a stop/start of
|
|
21
|
+
* the backend is picked up is unchanged. `expire` only bounds how long a stale
|
|
22
|
+
* value may be served when revalidation never runs.
|
|
15
23
|
*/
|
|
16
24
|
export async function isBackendReachable(): Promise<boolean> {
|
|
17
25
|
'use cache';
|
|
18
|
-
cacheLife(
|
|
26
|
+
cacheLife({ stale: 30, revalidate: 1, expire: 300 });
|
|
19
27
|
|
|
20
28
|
const apiUrl = process.env.VENDURE_SHOP_API_URL || process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL;
|
|
21
29
|
if (!apiUrl) return true;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { hasCapability } from '@/config/capabilities';
|
|
2
|
+
import type {
|
|
3
|
+
DefinitionNode,
|
|
4
|
+
DocumentNode,
|
|
5
|
+
FieldNode,
|
|
6
|
+
FragmentDefinitionNode,
|
|
7
|
+
SelectionNode,
|
|
8
|
+
SelectionSetNode,
|
|
9
|
+
} from 'graphql';
|
|
10
|
+
import { visit } from 'graphql';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A selection to drop, written as the path of field names from the operation
|
|
14
|
+
* root, e.g. `['product', 'customFields', 'configuratorSystemCode']`.
|
|
15
|
+
*/
|
|
16
|
+
export type FieldPath = readonly string[];
|
|
17
|
+
|
|
18
|
+
export interface CapabilityDocumentRule {
|
|
19
|
+
capability: string;
|
|
20
|
+
paths: readonly FieldPath[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CapabilityDocumentRegistration<T extends DocumentNode = DocumentNode> {
|
|
24
|
+
name: string;
|
|
25
|
+
fullDocument: T;
|
|
26
|
+
rules: readonly CapabilityDocumentRule[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Capability-owned storefront documents, used by the schema composition tests. */
|
|
30
|
+
export const STOREFRONT_CAPABILITY_DOCUMENTS: CapabilityDocumentRegistration[] = [];
|
|
31
|
+
|
|
32
|
+
function isField(selection: SelectionNode): selection is FieldNode {
|
|
33
|
+
return selection.kind === 'Field';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fieldName(field: FieldNode): string {
|
|
37
|
+
return field.name.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function addOperationFallback<T extends DocumentNode>(document: T): T {
|
|
41
|
+
return {
|
|
42
|
+
...document,
|
|
43
|
+
definitions: document.definitions.map((definition) => {
|
|
44
|
+
if (definition.kind !== 'OperationDefinition') return definition;
|
|
45
|
+
if (
|
|
46
|
+
definition.selectionSet.selections.some(
|
|
47
|
+
(selection) => isField(selection) && fieldName(selection) === '__typename',
|
|
48
|
+
)
|
|
49
|
+
) {
|
|
50
|
+
return definition;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
...definition,
|
|
54
|
+
selectionSet: {
|
|
55
|
+
...definition.selectionSet,
|
|
56
|
+
selections: [
|
|
57
|
+
...definition.selectionSet.selections,
|
|
58
|
+
{ kind: 'Field', name: { kind: 'Name', value: '__typename' } },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}),
|
|
63
|
+
} as T;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function fragmentSpreads(selectionSet: SelectionSetNode): string[] {
|
|
67
|
+
const names: string[] = [];
|
|
68
|
+
for (const selection of selectionSet.selections) {
|
|
69
|
+
if (selection.kind === 'FragmentSpread') {
|
|
70
|
+
names.push(selection.name.value);
|
|
71
|
+
} else if (selection.kind === 'Field' && selection.selectionSet) {
|
|
72
|
+
names.push(...fragmentSpreads(selection.selectionSet));
|
|
73
|
+
} else if (selection.kind === 'InlineFragment') {
|
|
74
|
+
names.push(...fragmentSpreads(selection.selectionSet));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return names;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function removeUnusedFragments(definitions: readonly DefinitionNode[]): readonly DefinitionNode[] {
|
|
81
|
+
const fragments = new Map<string, FragmentDefinitionNode>();
|
|
82
|
+
for (const definition of definitions) {
|
|
83
|
+
if (definition.kind === 'FragmentDefinition') {
|
|
84
|
+
fragments.set(definition.name.value, definition);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const used = new Set<string>();
|
|
89
|
+
const pending = definitions
|
|
90
|
+
.filter((definition) => definition.kind === 'OperationDefinition')
|
|
91
|
+
.flatMap((definition) => fragmentSpreads(definition.selectionSet));
|
|
92
|
+
while (pending.length > 0) {
|
|
93
|
+
const name = pending.pop()!;
|
|
94
|
+
if (used.has(name)) continue;
|
|
95
|
+
used.add(name);
|
|
96
|
+
const fragment = fragments.get(name);
|
|
97
|
+
if (fragment) pending.push(...fragmentSpreads(fragment.selectionSet));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return definitions.filter(
|
|
101
|
+
(definition) => definition.kind !== 'FragmentDefinition' || used.has(definition.name.value),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function removeUnusedVariables(definitions: readonly DefinitionNode[]): readonly DefinitionNode[] {
|
|
106
|
+
const used = new Set<string>();
|
|
107
|
+
visit({ kind: 'Document', definitions } as DocumentNode, {
|
|
108
|
+
VariableDefinition() {
|
|
109
|
+
return false;
|
|
110
|
+
},
|
|
111
|
+
Variable(node) {
|
|
112
|
+
used.add(node.name.value);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return definitions.map((definition) => {
|
|
117
|
+
if (definition.kind !== 'OperationDefinition' || !definition.variableDefinitions) {
|
|
118
|
+
return definition;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
...definition,
|
|
122
|
+
variableDefinitions: definition.variableDefinitions.filter((variable) =>
|
|
123
|
+
used.has(variable.variable.name.value),
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns `selectionSet` with `path` removed, or `null` when removing it would
|
|
131
|
+
* leave an empty selection set — GraphQL has no syntax for one, so the caller
|
|
132
|
+
* drops the parent field instead.
|
|
133
|
+
*/
|
|
134
|
+
function stripPath(selectionSet: SelectionSetNode, path: FieldPath): SelectionSetNode | null {
|
|
135
|
+
const [head, ...rest] = path;
|
|
136
|
+
const selections: SelectionNode[] = [];
|
|
137
|
+
|
|
138
|
+
for (const selection of selectionSet.selections) {
|
|
139
|
+
if (!isField(selection) || fieldName(selection) !== head) {
|
|
140
|
+
selections.push(selection);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (rest.length === 0) {
|
|
144
|
+
// Leaf reached: drop it by not re-adding it.
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!selection.selectionSet) {
|
|
148
|
+
selections.push(selection);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const nested = stripPath(selection.selectionSet, rest);
|
|
152
|
+
if (nested) {
|
|
153
|
+
selections.push({ ...selection, selectionSet: nested });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return selections.length > 0 ? { ...selectionSet, selections } : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Removes capability-gated selections from a GraphQL document.
|
|
162
|
+
*
|
|
163
|
+
* The server composes its schema from the capability modules the deployment is
|
|
164
|
+
* entitled to, so selecting a field owned by an absent module fails validation
|
|
165
|
+
* on every request. Rather than maintaining a hand-written copy of each
|
|
166
|
+
* document per composition, the full document stays the single typed source of
|
|
167
|
+
* truth and the selections a composition cannot serve are pruned from it here.
|
|
168
|
+
*
|
|
169
|
+
* The returned document keeps the input's TypeScript type: a pruned field is
|
|
170
|
+
* `undefined` at runtime, so consumers must treat capability-gated data as
|
|
171
|
+
* optional. That is the same contract they already need for a field the server
|
|
172
|
+
* may legitimately return as null.
|
|
173
|
+
*/
|
|
174
|
+
export function stripCapabilityFields<T extends DocumentNode>(document: T, paths: FieldPath[]): T {
|
|
175
|
+
if (paths.length === 0) {
|
|
176
|
+
return document;
|
|
177
|
+
}
|
|
178
|
+
let definitions = document.definitions;
|
|
179
|
+
for (const path of paths) {
|
|
180
|
+
definitions = definitions.map((definition) => {
|
|
181
|
+
if (definition.kind !== 'OperationDefinition' && definition.kind !== 'FragmentDefinition') {
|
|
182
|
+
return definition;
|
|
183
|
+
}
|
|
184
|
+
const stripped = stripPath(definition.selectionSet, path);
|
|
185
|
+
if (!stripped) {
|
|
186
|
+
// Every selection collapsed, which GraphQL cannot express. A
|
|
187
|
+
// prune list that empties a whole operation is a mistake in the
|
|
188
|
+
// capability mapping, not something to paper over by keeping
|
|
189
|
+
// the field that was supposed to be removed.
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Pruning "${path.join('.')}" would leave an empty selection set. ` +
|
|
192
|
+
'Split the document instead of pruning its only selection.',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return { ...definition, selectionSet: stripped };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const usedDefinitions = removeUnusedFragments(definitions);
|
|
199
|
+
return { ...document, definitions: removeUnusedVariables(usedDefinitions) } as T;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Convenience wrapper: prunes `paths` only when `granted` is false.
|
|
204
|
+
*/
|
|
205
|
+
export function whenGranted<T extends DocumentNode>(granted: boolean, document: T, paths: FieldPath[]): T {
|
|
206
|
+
return granted ? document : stripCapabilityFields(addOperationFallback(document), paths);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Registers and composes one typed storefront document from capability rules.
|
|
211
|
+
* The full typed document remains the source of truth. Missing grants prune
|
|
212
|
+
* only the selections that their modules own.
|
|
213
|
+
*/
|
|
214
|
+
export function composeCapabilityDocument<T extends DocumentNode>(
|
|
215
|
+
document: T,
|
|
216
|
+
rules: readonly CapabilityDocumentRule[],
|
|
217
|
+
): T {
|
|
218
|
+
const operation = document.definitions.find((definition) => definition.kind === 'OperationDefinition');
|
|
219
|
+
const name = operation?.name?.value;
|
|
220
|
+
if (!name) {
|
|
221
|
+
throw new Error('Capability-composed storefront documents must use a named operation.');
|
|
222
|
+
}
|
|
223
|
+
STOREFRONT_CAPABILITY_DOCUMENTS.push({ name, fullDocument: document, rules });
|
|
224
|
+
return composeCapabilityDocumentFor(document, rules, (capability) => hasCapability(capability));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Compose a registered document for an explicit grant set in tests. */
|
|
228
|
+
export function composeCapabilityDocumentFor<T extends DocumentNode>(
|
|
229
|
+
document: T,
|
|
230
|
+
rules: readonly CapabilityDocumentRule[],
|
|
231
|
+
granted: (capability: string) => boolean,
|
|
232
|
+
): T {
|
|
233
|
+
return rules.reduce(
|
|
234
|
+
(composed, rule) => whenGranted(granted(rule.capability), composed, [...rule.paths]),
|
|
235
|
+
document,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Registers a document owned by one capability. */
|
|
240
|
+
export function forCapability<T extends DocumentNode>(
|
|
241
|
+
capability: string,
|
|
242
|
+
document: T,
|
|
243
|
+
paths: readonly FieldPath[],
|
|
244
|
+
): T {
|
|
245
|
+
return composeCapabilityDocument(document, [{ capability, paths }]);
|
|
246
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSchema,
|
|
3
|
+
Kind,
|
|
4
|
+
parse,
|
|
5
|
+
print,
|
|
6
|
+
type DefinitionNode,
|
|
7
|
+
type DocumentNode,
|
|
8
|
+
type GraphQLSchema,
|
|
9
|
+
type InputObjectTypeDefinitionNode,
|
|
10
|
+
type InterfaceTypeDefinitionNode,
|
|
11
|
+
type ObjectTypeDefinitionNode,
|
|
12
|
+
} from 'graphql';
|
|
13
|
+
|
|
14
|
+
/** A capability-owned type, or a field on an otherwise shared type. */
|
|
15
|
+
export interface SchemaContribution {
|
|
16
|
+
/** Whole types the module adds to the schema. */
|
|
17
|
+
types?: readonly string[];
|
|
18
|
+
/** Fields the module adds to a type it shares with other modules. */
|
|
19
|
+
fields?: Readonly<Record<string, readonly string[]>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The two configurator schema sections, mirroring
|
|
24
|
+
* `libs/configurator/src/api/api-extensions.ts`. Removing both sections from
|
|
25
|
+
* the full schema reproduces a deployment without the configurator grant.
|
|
26
|
+
*/
|
|
27
|
+
export const CONFIGURATOR_FORMS_CONTRIBUTION: SchemaContribution = {
|
|
28
|
+
types: [
|
|
29
|
+
'FieldType',
|
|
30
|
+
'ConfiguratorField',
|
|
31
|
+
'ConfiguratorFieldTranslation',
|
|
32
|
+
'ConfiguratorFieldOption',
|
|
33
|
+
'ConfiguratorFieldOptionTranslation',
|
|
34
|
+
'ConfiguratorModule',
|
|
35
|
+
'ConfiguratorModuleTranslation',
|
|
36
|
+
'ConfiguratorSystem',
|
|
37
|
+
'ConfiguratorSystemTranslation',
|
|
38
|
+
'ProductConfiguration',
|
|
39
|
+
'ProductConfigurationFieldValue',
|
|
40
|
+
'ProductConfigurationEvaluation',
|
|
41
|
+
'EvaluatedField',
|
|
42
|
+
'EvaluatedFieldOption',
|
|
43
|
+
'EvaluatedFieldMessage',
|
|
44
|
+
'ConfiguratorValueInput',
|
|
45
|
+
'EvaluateProductConfigurationInput',
|
|
46
|
+
'AddProductConfigurationToOrderInput',
|
|
47
|
+
'UpdateProductConfigurationOnOrderInput',
|
|
48
|
+
],
|
|
49
|
+
fields: {
|
|
50
|
+
Query: ['configuratorSystem', 'configuratorSystems', 'evaluateProductConfiguration'],
|
|
51
|
+
Mutation: [
|
|
52
|
+
'addProductConfigurationToOrder',
|
|
53
|
+
'updateProductConfigurationOnOrder',
|
|
54
|
+
'removeProductConfigurationFromOrder',
|
|
55
|
+
],
|
|
56
|
+
Order: ['productConfigurations'],
|
|
57
|
+
OrderLineCustomFields: ['productConfiguration', 'productConfigurationId'],
|
|
58
|
+
ProductCustomFields: ['configuratorSystemCode'],
|
|
59
|
+
ProductFilterParameter: ['configuratorSystemCode'],
|
|
60
|
+
ProductSortParameter: ['configuratorSystemCode'],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const CONFIGURATOR_RULES_CONTRIBUTION: SchemaContribution = {
|
|
65
|
+
types: [
|
|
66
|
+
'ProductConfigurationLine',
|
|
67
|
+
'EvaluatedProductConfigurationLine',
|
|
68
|
+
'ProductConfigurationEvaluationTotals',
|
|
69
|
+
'CalculationError',
|
|
70
|
+
'ExecutedRule',
|
|
71
|
+
],
|
|
72
|
+
fields: {
|
|
73
|
+
ProductConfigurationEvaluation: ['lines', 'errors', 'executedRules', 'totals'],
|
|
74
|
+
ProductConfiguration: ['lines'],
|
|
75
|
+
OrderLineCustomFields: ['productConfigurationLine', 'productConfigurationLineId'],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Shop API fields contributed by capability modules used in storefront documents. */
|
|
80
|
+
export const STOREFRONT_CAPABILITY_CONTRIBUTIONS: Readonly<Record<string, SchemaContribution>> = {
|
|
81
|
+
'org.accounts': {
|
|
82
|
+
fields: {
|
|
83
|
+
Query: [
|
|
84
|
+
'activeCompanyUser',
|
|
85
|
+
'companyAccount',
|
|
86
|
+
'shopCompanyUsers',
|
|
87
|
+
'companyRoles',
|
|
88
|
+
'companyAddresses',
|
|
89
|
+
'companyPaymentMethods',
|
|
90
|
+
'companyInvitation',
|
|
91
|
+
'companyOrders',
|
|
92
|
+
],
|
|
93
|
+
Mutation: [
|
|
94
|
+
'registerCompany',
|
|
95
|
+
'updateCompanyAccount',
|
|
96
|
+
'createCompanyAddress',
|
|
97
|
+
'updateCompanyAddress',
|
|
98
|
+
'deleteCompanyAddress',
|
|
99
|
+
'inviteCompanyUser',
|
|
100
|
+
'updateCompanyUser',
|
|
101
|
+
'removeCompanyUser',
|
|
102
|
+
'assignCompanyUserRole',
|
|
103
|
+
'acceptCompanyInvitation',
|
|
104
|
+
],
|
|
105
|
+
Customer: ['companyUsers'],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
'org.hierarchy': {
|
|
109
|
+
fields: {
|
|
110
|
+
Query: ['companyBusinessUnits'],
|
|
111
|
+
Mutation: [
|
|
112
|
+
'createCompanyBusinessUnit',
|
|
113
|
+
'updateCompanyBusinessUnit',
|
|
114
|
+
'deleteCompanyBusinessUnit',
|
|
115
|
+
'switchBusinessUnit',
|
|
116
|
+
],
|
|
117
|
+
Company: ['businessUnits'],
|
|
118
|
+
CompanyAccount: ['activeBusinessUnit'],
|
|
119
|
+
CompanyUser: ['businessUnits'],
|
|
120
|
+
CompanyAddress: ['businessUnit'],
|
|
121
|
+
CreateCompanyAddressShopInput: ['businessUnitId'],
|
|
122
|
+
UpdateCompanyAddressShopInput: ['businessUnitId'],
|
|
123
|
+
InviteCompanyUserInput: ['businessUnitIds'],
|
|
124
|
+
UpdateCompanyUserShopInput: ['businessUnitIds'],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
'tax-id.validation': {
|
|
128
|
+
fields: {
|
|
129
|
+
Query: ['taxIds'],
|
|
130
|
+
Mutation: ['addTaxId', 'removeTaxId', 'setDefaultTaxId'],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
'quote.requests': {
|
|
134
|
+
fields: {
|
|
135
|
+
Query: ['myQuotes', 'myQuote', 'quoteStockCheck'],
|
|
136
|
+
Mutation: [
|
|
137
|
+
'requestQuote',
|
|
138
|
+
'requestQuoteFromCart',
|
|
139
|
+
'submitQuoteRequest',
|
|
140
|
+
'acceptQuote',
|
|
141
|
+
'rejectQuote',
|
|
142
|
+
'acceptAndConvertQuote',
|
|
143
|
+
'cancelQuoteCheckout',
|
|
144
|
+
],
|
|
145
|
+
OrderCustomFields: ['sourceQuoteId'],
|
|
146
|
+
OrderFilterParameter: ['sourceQuoteId'],
|
|
147
|
+
OrderSortParameter: ['sourceQuoteId'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
'quote.negotiation': {
|
|
151
|
+
fields: {
|
|
152
|
+
Mutation: ['requestQuoteChanges', 'addQuoteComment'],
|
|
153
|
+
Quote: ['comments', 'versions'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
'shopping-list.personal': {
|
|
157
|
+
fields: {
|
|
158
|
+
Query: ['activeUserShoppingList', 'activeUserShoppingLists'],
|
|
159
|
+
Mutation: [
|
|
160
|
+
'addToShoppingList',
|
|
161
|
+
'removeFromShoppingList',
|
|
162
|
+
'updateShoppingListItem',
|
|
163
|
+
'createShoppingList',
|
|
164
|
+
'updateShoppingList',
|
|
165
|
+
'deleteShoppingList',
|
|
166
|
+
'addShoppingListItemsToOrder',
|
|
167
|
+
'moveShoppingListItems',
|
|
168
|
+
],
|
|
169
|
+
ProductVariant: ['appearsInShoppingLists'],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
'shopping-list.managed': {
|
|
173
|
+
fields: {
|
|
174
|
+
Query: ['publicShoppingList'],
|
|
175
|
+
Mutation: ['setShoppingListVisibility'],
|
|
176
|
+
ShoppingList: ['isPublic', 'publicCode', 'customer'],
|
|
177
|
+
ShoppingListItem: ['purchased', 'lastPurchasedAt'],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
'quick-order': {
|
|
181
|
+
fields: {
|
|
182
|
+
Query: ['quickOrderSearch'],
|
|
183
|
+
Mutation: ['validateQuickOrderItems', 'addQuickOrderItemsToOrder'],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
reorder: {
|
|
187
|
+
fields: {
|
|
188
|
+
Query: ['previewReorder'],
|
|
189
|
+
Mutation: ['addReorderToActiveOrder'],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
'store-credit.balance': {
|
|
193
|
+
fields: {
|
|
194
|
+
Query: ['myStoreCredits'],
|
|
195
|
+
Mutation: [
|
|
196
|
+
'applyStoreCreditToOrder',
|
|
197
|
+
'removeStoreCreditFromOrder',
|
|
198
|
+
'claimStoreCredit',
|
|
199
|
+
'addPaymentToOrderWithStoreCredit',
|
|
200
|
+
],
|
|
201
|
+
Order: ['storeCreditsApplied', 'totalWithTaxAfterStoreCredit'],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
'store-credit.gift-cards': {
|
|
205
|
+
fields: {
|
|
206
|
+
Query: ['giftCardTemplates'],
|
|
207
|
+
Mutation: ['addGiftCardToOrder'],
|
|
208
|
+
OrderLine: ['giftCardInput'],
|
|
209
|
+
ProductCustomFields: ['isGiftCard'],
|
|
210
|
+
ProductFilterParameter: ['isGiftCard'],
|
|
211
|
+
ProductSortParameter: ['isGiftCard'],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
'packaging-units': {
|
|
215
|
+
fields: {
|
|
216
|
+
ProductVariant: ['packagingUnits'],
|
|
217
|
+
OrderLineCustomFields: ['packagingUnit', 'packagingUnitConversionFactor'],
|
|
218
|
+
OrderLineCustomFieldsInput: [
|
|
219
|
+
'packagingUnitId',
|
|
220
|
+
'packagingUnitConversionFactor',
|
|
221
|
+
'packagingUnitName',
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
'shared-carts.collaboration': {
|
|
226
|
+
fields: {
|
|
227
|
+
Query: ['mySharedCarts', 'sharedCart', 'companySharedCarts'],
|
|
228
|
+
Mutation: [
|
|
229
|
+
'createSharedCart',
|
|
230
|
+
'updateSharedCart',
|
|
231
|
+
'deleteSharedCart',
|
|
232
|
+
'addItemToSharedCart',
|
|
233
|
+
'adjustSharedCartLine',
|
|
234
|
+
'removeSharedCartLine',
|
|
235
|
+
'addSharedCartMember',
|
|
236
|
+
'removeSharedCartMember',
|
|
237
|
+
'setSharedCartMemberRole',
|
|
238
|
+
'transferSharedCartOwnership',
|
|
239
|
+
'initiateSharedCartCheckout',
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
'content.history': { fields: { Query: ['contentVersioningProductVariant'] } },
|
|
244
|
+
'approval.orders': {
|
|
245
|
+
fields: {
|
|
246
|
+
Query: ['myApprovalRequests', 'approvalHistoryForEntity'],
|
|
247
|
+
Mutation: ['submitApprovalDecision', 'batchSubmitApprovalDecisions'],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
'pricing.contract': {
|
|
251
|
+
fields: {
|
|
252
|
+
ProductVariant: ['priceTiers'],
|
|
253
|
+
ShopProductVariantPackagingUnit: ['priceTiers'],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
'search.foundation': {
|
|
257
|
+
fields: {
|
|
258
|
+
Query: ['searchCollections'],
|
|
259
|
+
SearchResult: ['highlights'],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
'search.analytics.clickhouse': {
|
|
263
|
+
fields: {
|
|
264
|
+
Mutation: ['logSearchListViewed', 'logSearchResultClicked'],
|
|
265
|
+
SearchResponse: ['queryId'],
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
'configurator.forms': {
|
|
269
|
+
fields: {
|
|
270
|
+
...CONFIGURATOR_FORMS_CONTRIBUTION.fields,
|
|
271
|
+
...CONFIGURATOR_RULES_CONTRIBUTION.fields,
|
|
272
|
+
OrderLineCustomFields: [
|
|
273
|
+
'productConfiguration',
|
|
274
|
+
'productConfigurationId',
|
|
275
|
+
'productConfigurationLine',
|
|
276
|
+
'productConfigurationLineId',
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
punchout: {
|
|
281
|
+
fields: {
|
|
282
|
+
Query: ['activePunchoutSession'],
|
|
283
|
+
Mutation: ['activatePunchoutSession', 'transferPunchoutCart'],
|
|
284
|
+
AuthenticationInput: ['punchout'],
|
|
285
|
+
ProductCustomFields: ['unspscCode'],
|
|
286
|
+
ProductFilterParameter: ['unspscCode'],
|
|
287
|
+
ProductSortParameter: ['unspscCode'],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/** Build the Shop API schema for an explicit grant set. */
|
|
293
|
+
export function buildSchemaForCapabilities(fullSdl: string, grants: ReadonlySet<string>): GraphQLSchema {
|
|
294
|
+
return buildSchemaWithout(
|
|
295
|
+
fullSdl,
|
|
296
|
+
Object.entries(STOREFRONT_CAPABILITY_CONTRIBUTIONS)
|
|
297
|
+
.filter(([capability]) => !grants.has(capability))
|
|
298
|
+
.map(([, contribution]) => contribution),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function removeContribution(document: DocumentNode, contribution: SchemaContribution): DocumentNode {
|
|
303
|
+
const droppedTypes = new Set(contribution.types ?? []);
|
|
304
|
+
const droppedFields = contribution.fields ?? {};
|
|
305
|
+
|
|
306
|
+
const definitions: DefinitionNode[] = [];
|
|
307
|
+
for (const definition of document.definitions) {
|
|
308
|
+
if (!('name' in definition) || !definition.name) {
|
|
309
|
+
definitions.push(definition);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const typeName = definition.name.value;
|
|
313
|
+
if (droppedTypes.has(typeName)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const fieldsToDrop = droppedFields[typeName];
|
|
317
|
+
if (!fieldsToDrop) {
|
|
318
|
+
definitions.push(definition);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (
|
|
322
|
+
definition.kind === Kind.OBJECT_TYPE_DEFINITION ||
|
|
323
|
+
definition.kind === Kind.INTERFACE_TYPE_DEFINITION
|
|
324
|
+
) {
|
|
325
|
+
const node = definition as ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode;
|
|
326
|
+
definitions.push({
|
|
327
|
+
...node,
|
|
328
|
+
fields: (node.fields ?? []).filter((field) => !fieldsToDrop.includes(field.name.value)),
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (definition.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) {
|
|
333
|
+
const node = definition as InputObjectTypeDefinitionNode;
|
|
334
|
+
definitions.push({
|
|
335
|
+
...node,
|
|
336
|
+
fields: (node.fields ?? []).filter((field) => !fieldsToDrop.includes(field.name.value)),
|
|
337
|
+
});
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
definitions.push(definition);
|
|
341
|
+
}
|
|
342
|
+
return { ...document, definitions };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Builds the Shop API schema a deployment serves when the listed capability
|
|
347
|
+
* contributions are absent.
|
|
348
|
+
*/
|
|
349
|
+
export function buildSchemaWithout(
|
|
350
|
+
fullSdl: string,
|
|
351
|
+
contributions: readonly SchemaContribution[],
|
|
352
|
+
): GraphQLSchema {
|
|
353
|
+
let document = parse(fullSdl);
|
|
354
|
+
for (const contribution of contributions) {
|
|
355
|
+
document = removeContribution(document, contribution);
|
|
356
|
+
}
|
|
357
|
+
return buildSchema(print(document));
|
|
358
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Makes capability-pruned quote activity safe for UI consumers. */
|
|
2
|
+
export function getQuoteNegotiationData<Comment, Version>(quote: {
|
|
3
|
+
comments?: Comment[];
|
|
4
|
+
versions?: Version[];
|
|
5
|
+
}): { comments: Comment[]; versions: Version[] } {
|
|
6
|
+
return {
|
|
7
|
+
comments: quote.comments ?? [],
|
|
8
|
+
versions: quote.versions ?? [],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Makes capability-pruned search highlights safe for product cards. */
|
|
13
|
+
export function getSearchHighlights<Highlight>(product: { highlights?: Highlight[] }): Highlight[] {
|
|
14
|
+
return product.highlights ?? [];
|
|
15
|
+
}
|