@foir/sdk 0.1.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 +110 -0
- package/dist/auth.d.ts +305 -0
- package/dist/auth.js +309 -0
- package/dist/chunk-ADO5FJNM.js +30 -0
- package/dist/client.d.ts +83 -0
- package/dist/client.js +81 -0
- package/dist/codegen/block-registry.d.ts +27 -0
- package/dist/codegen/block-registry.js +35 -0
- package/dist/codegen/block-scaffold.d.ts +28 -0
- package/dist/codegen/block-scaffold.js +45 -0
- package/dist/codegen/index.d.ts +65 -0
- package/dist/codegen/index.js +80 -0
- package/dist/content.d.ts +96 -0
- package/dist/content.js +161 -0
- package/dist/index.d.ts +394 -0
- package/dist/index.js +476 -0
- package/dist/media.d.ts +234 -0
- package/dist/media.js +399 -0
- package/dist/realtime.d.ts +111 -0
- package/dist/realtime.js +168 -0
- package/dist/server.d.ts +457 -0
- package/dist/server.js +296 -0
- package/dist/shared-DkWlEd2Z.d.ts +18 -0
- package/package.json +136 -0
package/dist/auth.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// src/graphql/foir-request.ts
|
|
2
|
+
import { print } from "graphql";
|
|
3
|
+
function isFoirPreview(env) {
|
|
4
|
+
return env.FOIR_PREVIEW === "true";
|
|
5
|
+
}
|
|
6
|
+
var FoirRequestError = class extends Error {
|
|
7
|
+
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
8
|
+
status;
|
|
9
|
+
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
10
|
+
graphQLErrors;
|
|
11
|
+
cause;
|
|
12
|
+
constructor(message, options) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "FoirRequestError";
|
|
15
|
+
this.cause = options?.cause;
|
|
16
|
+
this.status = options?.status;
|
|
17
|
+
this.graphQLErrors = options?.graphQLErrors;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
async function foirRequest(env, document, variables, options) {
|
|
21
|
+
const endpoint = env.PLATFORM_API_URL;
|
|
22
|
+
const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
|
|
23
|
+
if (!endpoint || !apiKey) {
|
|
24
|
+
throw new FoirRequestError(
|
|
25
|
+
"[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const headers = {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"x-api-key": apiKey
|
|
31
|
+
};
|
|
32
|
+
if (isFoirPreview(env)) {
|
|
33
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
34
|
+
}
|
|
35
|
+
if (options?.bearerToken) {
|
|
36
|
+
headers["Authorization"] = `Bearer ${options.bearerToken}`;
|
|
37
|
+
}
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await fetch(endpoint, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers,
|
|
43
|
+
body: JSON.stringify({ query: print(document), variables })
|
|
44
|
+
});
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw new FoirRequestError("[foir] network error", { cause: err });
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
|
|
50
|
+
status: res.status
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const json = await res.json();
|
|
54
|
+
if (json.errors?.length) {
|
|
55
|
+
const msg = json.errors.map((e) => e.message).join("; ");
|
|
56
|
+
if (json.data) {
|
|
57
|
+
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
58
|
+
} else {
|
|
59
|
+
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
60
|
+
graphQLErrors: json.errors
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!json.data) {
|
|
65
|
+
throw new FoirRequestError("[foir] empty GraphQL response");
|
|
66
|
+
}
|
|
67
|
+
return json.data;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/auth/operations.ts
|
|
71
|
+
import { parse } from "graphql";
|
|
72
|
+
var AUTH_RESULT_FIELDS = "token refreshToken customerId email";
|
|
73
|
+
var CustomerLoginDocument = parse(
|
|
74
|
+
`mutation CustomerLogin($email: String!, $password: String!) {
|
|
75
|
+
customerLogin(email: $email, password: $password) { ${AUTH_RESULT_FIELDS} }
|
|
76
|
+
}`
|
|
77
|
+
);
|
|
78
|
+
var CustomerRegisterDocument = parse(
|
|
79
|
+
`mutation CustomerRegister($email: String!, $password: String!) {
|
|
80
|
+
customerRegister(email: $email, password: $password) { ${AUTH_RESULT_FIELDS} }
|
|
81
|
+
}`
|
|
82
|
+
);
|
|
83
|
+
var CustomerRefreshTokenDocument = parse(
|
|
84
|
+
`mutation CustomerRefreshToken($refreshToken: String!) {
|
|
85
|
+
customerRefreshToken(refreshToken: $refreshToken) { ${AUTH_RESULT_FIELDS} }
|
|
86
|
+
}`
|
|
87
|
+
);
|
|
88
|
+
var CustomerRequestOTPDocument = parse(
|
|
89
|
+
`mutation CustomerRequestOTP($email: String!) {
|
|
90
|
+
customerRequestOTP(email: $email)
|
|
91
|
+
}`
|
|
92
|
+
);
|
|
93
|
+
var CustomerLoginOTPDocument = parse(
|
|
94
|
+
`mutation CustomerLoginOTP($email: String!, $otp: String!) {
|
|
95
|
+
customerLoginOTP(email: $email, otp: $otp) { ${AUTH_RESULT_FIELDS} }
|
|
96
|
+
}`
|
|
97
|
+
);
|
|
98
|
+
var CustomerLoginWithProviderDocument = parse(
|
|
99
|
+
`mutation CustomerLoginWithProvider($input: ProviderLoginInput!) {
|
|
100
|
+
customerLoginWithProvider(input: $input) { redirectUrl token }
|
|
101
|
+
}`
|
|
102
|
+
);
|
|
103
|
+
var CustomerProviderCallbackDocument = parse(
|
|
104
|
+
`mutation CustomerProviderCallback($input: ProviderCallbackInput!) {
|
|
105
|
+
customerProviderCallback(input: $input) { ${AUTH_RESULT_FIELDS} }
|
|
106
|
+
}`
|
|
107
|
+
);
|
|
108
|
+
var CustomerProviderVerifyOTPDocument = parse(
|
|
109
|
+
`mutation CustomerProviderVerifyOTP($input: ProviderOTPVerifyInput!) {
|
|
110
|
+
customerProviderVerifyOTP(input: $input) { ${AUTH_RESULT_FIELDS} }
|
|
111
|
+
}`
|
|
112
|
+
);
|
|
113
|
+
var AuthProvidersDocument = parse(
|
|
114
|
+
`query AuthProviders {
|
|
115
|
+
authProviders { key name type isDefault }
|
|
116
|
+
}`
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// src/auth/auth.ts
|
|
120
|
+
var TOKEN_KEY = "foir:token";
|
|
121
|
+
var REFRESH_KEY = "foir:refreshToken";
|
|
122
|
+
var CUSTOMER_ID_KEY = "foir:customerId";
|
|
123
|
+
var EMAIL_KEY = "foir:email";
|
|
124
|
+
async function customerLogin(env, vars) {
|
|
125
|
+
return (await foirRequest(env, CustomerLoginDocument, vars)).customerLogin;
|
|
126
|
+
}
|
|
127
|
+
async function customerRegister(env, vars) {
|
|
128
|
+
return (await foirRequest(env, CustomerRegisterDocument, vars)).customerRegister;
|
|
129
|
+
}
|
|
130
|
+
async function customerRequestOTP(env, email) {
|
|
131
|
+
return (await foirRequest(env, CustomerRequestOTPDocument, { email })).customerRequestOTP;
|
|
132
|
+
}
|
|
133
|
+
async function customerLoginOTP(env, vars) {
|
|
134
|
+
return (await foirRequest(env, CustomerLoginOTPDocument, vars)).customerLoginOTP;
|
|
135
|
+
}
|
|
136
|
+
async function customerRefreshToken(env, refreshToken) {
|
|
137
|
+
return (await foirRequest(env, CustomerRefreshTokenDocument, { refreshToken })).customerRefreshToken;
|
|
138
|
+
}
|
|
139
|
+
async function customerLoginWithProvider(env, input) {
|
|
140
|
+
return (await foirRequest(env, CustomerLoginWithProviderDocument, { input })).customerLoginWithProvider;
|
|
141
|
+
}
|
|
142
|
+
async function customerProviderCallback(env, input) {
|
|
143
|
+
return (await foirRequest(env, CustomerProviderCallbackDocument, { input })).customerProviderCallback;
|
|
144
|
+
}
|
|
145
|
+
async function customerProviderVerifyOTP(env, input) {
|
|
146
|
+
return (await foirRequest(env, CustomerProviderVerifyOTPDocument, { input })).customerProviderVerifyOTP;
|
|
147
|
+
}
|
|
148
|
+
async function getAuthProviders(env) {
|
|
149
|
+
return (await foirRequest(env, AuthProvidersDocument, {})).authProviders;
|
|
150
|
+
}
|
|
151
|
+
async function saveSession(adapter, result) {
|
|
152
|
+
await adapter.set(TOKEN_KEY, result.token);
|
|
153
|
+
await adapter.set(REFRESH_KEY, result.refreshToken);
|
|
154
|
+
await adapter.set(CUSTOMER_ID_KEY, result.customerId);
|
|
155
|
+
await adapter.set(EMAIL_KEY, result.email);
|
|
156
|
+
return { id: result.customerId, email: result.email };
|
|
157
|
+
}
|
|
158
|
+
async function clearSession(adapter) {
|
|
159
|
+
await adapter.unset(TOKEN_KEY);
|
|
160
|
+
await adapter.unset(REFRESH_KEY);
|
|
161
|
+
await adapter.unset(CUSTOMER_ID_KEY);
|
|
162
|
+
await adapter.unset(EMAIL_KEY);
|
|
163
|
+
}
|
|
164
|
+
async function getSessionToken(adapter) {
|
|
165
|
+
return await adapter.get(TOKEN_KEY) ?? null;
|
|
166
|
+
}
|
|
167
|
+
async function getSessionCustomer(adapter) {
|
|
168
|
+
const id = await adapter.get(CUSTOMER_ID_KEY) ?? null;
|
|
169
|
+
const email = await adapter.get(EMAIL_KEY) ?? null;
|
|
170
|
+
if (!id) return null;
|
|
171
|
+
return { id, email: email ?? "" };
|
|
172
|
+
}
|
|
173
|
+
async function loginWithPassword(adapter, env, vars) {
|
|
174
|
+
return saveSession(adapter, await customerLogin(env, vars));
|
|
175
|
+
}
|
|
176
|
+
async function registerWithPassword(adapter, env, vars) {
|
|
177
|
+
return saveSession(adapter, await customerRegister(env, vars));
|
|
178
|
+
}
|
|
179
|
+
async function loginWithOTP(adapter, env, vars) {
|
|
180
|
+
return saveSession(adapter, await customerLoginOTP(env, vars));
|
|
181
|
+
}
|
|
182
|
+
async function refreshSession(adapter, env) {
|
|
183
|
+
const refreshToken = await adapter.get(REFRESH_KEY) ?? null;
|
|
184
|
+
if (!refreshToken) return null;
|
|
185
|
+
try {
|
|
186
|
+
const result = await customerRefreshToken(env, refreshToken);
|
|
187
|
+
await saveSession(adapter, result);
|
|
188
|
+
return { token: result.token, refreshToken: result.refreshToken };
|
|
189
|
+
} catch {
|
|
190
|
+
await clearSession(adapter);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function isAuthExpiredError(err) {
|
|
195
|
+
if (!(err instanceof FoirRequestError)) return false;
|
|
196
|
+
if (err.status === 401) return true;
|
|
197
|
+
return (err.graphQLErrors ?? []).some(
|
|
198
|
+
(e) => /authentication required/i.test(e.message)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
async function authedRequest(adapter, env, document, variables) {
|
|
202
|
+
const token = await getSessionToken(adapter);
|
|
203
|
+
try {
|
|
204
|
+
return await foirRequest(env, document, variables, {
|
|
205
|
+
bearerToken: token ?? void 0
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (!isAuthExpiredError(err)) throw err;
|
|
209
|
+
const refreshed = await refreshSession(adapter, env);
|
|
210
|
+
if (!refreshed) throw err;
|
|
211
|
+
return foirRequest(env, document, variables, {
|
|
212
|
+
bearerToken: refreshed.token
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function requireAuth(adapter, options) {
|
|
217
|
+
const token = await getSessionToken(adapter);
|
|
218
|
+
const customer = await getSessionCustomer(adapter);
|
|
219
|
+
if (!token || !customer) {
|
|
220
|
+
await options.onUnauthenticated();
|
|
221
|
+
throw new Error("[foir] onUnauthenticated must not return");
|
|
222
|
+
}
|
|
223
|
+
if (options.resolveUser) {
|
|
224
|
+
return options.resolveUser({ token, customer });
|
|
225
|
+
}
|
|
226
|
+
return { id: customer.id, email: customer.email, isAdmin: false };
|
|
227
|
+
}
|
|
228
|
+
async function requireAdmin(adapter, options) {
|
|
229
|
+
const user = await requireAuth(adapter, options);
|
|
230
|
+
if (!user.isAdmin) {
|
|
231
|
+
await options.onForbidden();
|
|
232
|
+
throw new Error("[foir] onForbidden must not return");
|
|
233
|
+
}
|
|
234
|
+
return user;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/auth/hosted.ts
|
|
238
|
+
var DEFAULT_COOKIE_NAME = "foir_customer_session";
|
|
239
|
+
var DEFAULT_SESSION_PATH = "/session/token";
|
|
240
|
+
function hostedLoginUrl(config, returnTo) {
|
|
241
|
+
const dest = new URL(config.loginOrigin);
|
|
242
|
+
if (returnTo && returnTo !== "/") {
|
|
243
|
+
dest.searchParams.set("returnTo", returnTo);
|
|
244
|
+
}
|
|
245
|
+
return dest.toString();
|
|
246
|
+
}
|
|
247
|
+
async function fetchHostedSession(request, config) {
|
|
248
|
+
const cookieName = config.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
249
|
+
const sessionPath = config.sessionPath ?? DEFAULT_SESSION_PATH;
|
|
250
|
+
const cookie = request.headers.get("Cookie") ?? "";
|
|
251
|
+
const match = cookie.match(
|
|
252
|
+
new RegExp(`(?:^|;\\s*)${escapeRegExp(cookieName)}=([^;]+)`)
|
|
253
|
+
);
|
|
254
|
+
if (!match) return null;
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`${config.loginOrigin}${sessionPath}`, {
|
|
257
|
+
headers: {
|
|
258
|
+
Cookie: `${cookieName}=${match[1]}`,
|
|
259
|
+
"X-Foir-Session": "1"
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
if (!res.ok) return null;
|
|
263
|
+
const json = await res.json();
|
|
264
|
+
if (!json.access_token || !json.customer_id) return null;
|
|
265
|
+
return {
|
|
266
|
+
token: json.access_token,
|
|
267
|
+
customerId: json.customer_id,
|
|
268
|
+
email: json.email ?? ""
|
|
269
|
+
};
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function escapeRegExp(s) {
|
|
275
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
276
|
+
}
|
|
277
|
+
export {
|
|
278
|
+
AuthProvidersDocument,
|
|
279
|
+
CustomerLoginDocument,
|
|
280
|
+
CustomerLoginOTPDocument,
|
|
281
|
+
CustomerLoginWithProviderDocument,
|
|
282
|
+
CustomerProviderCallbackDocument,
|
|
283
|
+
CustomerProviderVerifyOTPDocument,
|
|
284
|
+
CustomerRefreshTokenDocument,
|
|
285
|
+
CustomerRegisterDocument,
|
|
286
|
+
CustomerRequestOTPDocument,
|
|
287
|
+
authedRequest,
|
|
288
|
+
clearSession,
|
|
289
|
+
customerLogin,
|
|
290
|
+
customerLoginOTP,
|
|
291
|
+
customerLoginWithProvider,
|
|
292
|
+
customerProviderCallback,
|
|
293
|
+
customerProviderVerifyOTP,
|
|
294
|
+
customerRefreshToken,
|
|
295
|
+
customerRegister,
|
|
296
|
+
customerRequestOTP,
|
|
297
|
+
fetchHostedSession,
|
|
298
|
+
getAuthProviders,
|
|
299
|
+
getSessionCustomer,
|
|
300
|
+
getSessionToken,
|
|
301
|
+
hostedLoginUrl,
|
|
302
|
+
loginWithOTP,
|
|
303
|
+
loginWithPassword,
|
|
304
|
+
refreshSession,
|
|
305
|
+
registerWithPassword,
|
|
306
|
+
requireAdmin,
|
|
307
|
+
requireAuth,
|
|
308
|
+
saveSession
|
|
309
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/codegen/shared.ts
|
|
2
|
+
import { isUnionType } from "graphql";
|
|
3
|
+
var DEFAULT_UNIONS = ["Block"];
|
|
4
|
+
var DEFAULT_GRAPHQL_IMPORT = "./graphql";
|
|
5
|
+
var DEFAULT_SDK_IMPORT = "@foir/sdk/content";
|
|
6
|
+
function collectBlockMembers(schema, unionNames) {
|
|
7
|
+
const members = /* @__PURE__ */ new Set();
|
|
8
|
+
const missing = [];
|
|
9
|
+
for (const name of unionNames) {
|
|
10
|
+
const type = schema.getType(name);
|
|
11
|
+
if (!type || !isUnionType(type)) {
|
|
12
|
+
missing.push(name);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
for (const member of type.getTypes()) members.add(member.name);
|
|
16
|
+
}
|
|
17
|
+
if (members.size === 0) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`[foir codegen] no union members found. Looked for union(s): ${unionNames.join(", ")}` + (missing.length ? ` (not found or not a union: ${missing.join(", ")})` : "") + `. Run \`foir push\` and confirm the block union name.`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return [...members].sort();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_UNIONS,
|
|
27
|
+
DEFAULT_GRAPHQL_IMPORT,
|
|
28
|
+
DEFAULT_SDK_IMPORT,
|
|
29
|
+
collectBlockMembers
|
|
30
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Foir GraphQL client — a single typed fetch wrapper for SSR/edge loaders.
|
|
5
|
+
*
|
|
6
|
+
* No GraphQL client (Apollo/urql) required: the Foir public API is a plain
|
|
7
|
+
* `POST` with an `x-api-key` header, and the dominant consumers (Hydrogen on
|
|
8
|
+
* Oxygen, React Router loaders, TanStack Start on CF Workers) fetch in
|
|
9
|
+
* server-only loaders and render — so Apollo's `InMemoryCache`/`useQuery`
|
|
10
|
+
* reactivity is dead weight, and a shared cache on a stateless multi-tenant
|
|
11
|
+
* edge worker is a cross-tenant hazard. This helper carries only the
|
|
12
|
+
* Foir-specific transport concerns (auth header, preview schema channel,
|
|
13
|
+
* error shape). Consumers on a GraphQL client (Apollo / urql / react-query /
|
|
14
|
+
* graphql-request) use the generated `TypedDocumentNode`s directly with their
|
|
15
|
+
* own client and replicate the two Foir-specific headers below — the
|
|
16
|
+
* `x-api-key` and, in preview, `X-Foir-Schema-Channel: draft`.
|
|
17
|
+
*
|
|
18
|
+
* Deduplicates the hand-copied `lib/foir/client.ts` that shipped identically
|
|
19
|
+
* across the Foir storefronts.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface FoirEnv {
|
|
23
|
+
PLATFORM_API_URL?: string;
|
|
24
|
+
PLATFORM_API_KEY?: string;
|
|
25
|
+
PLATFORM_PREVIEW_API_KEY?: string;
|
|
26
|
+
FOIR_PREVIEW?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns true when `FOIR_PREVIEW=true` is set in the environment.
|
|
30
|
+
*
|
|
31
|
+
* Preview is selected at the environment level, not per-request: load a
|
|
32
|
+
* preview env file that sets `FOIR_PREVIEW=true` + a `sk_…`
|
|
33
|
+
* `PLATFORM_PREVIEW_API_KEY` with `drafts:read` instead of the default env.
|
|
34
|
+
* The site-wide mode keeps draft semantics consistent — every loader sees
|
|
35
|
+
* the same preview/published view of the world. Intended for non-production
|
|
36
|
+
* surfaces only.
|
|
37
|
+
*/
|
|
38
|
+
declare function isFoirPreview(env: FoirEnv): boolean;
|
|
39
|
+
/** Default Foir CDN origin used to absolutise relative media URLs. */
|
|
40
|
+
declare const FOIR_CDN_BASE = "https://cdn.foir.dev/";
|
|
41
|
+
/**
|
|
42
|
+
* Foir returns `hlsManifestUrl` as a relative path while every other media
|
|
43
|
+
* URL is absolute. Prefix relative paths with the CDN host so the browser
|
|
44
|
+
* doesn't request them from the consumer's own origin. Already-absolute URLs
|
|
45
|
+
* pass through untouched. `cdnBase` is overridable for non-default CDN hosts.
|
|
46
|
+
*/
|
|
47
|
+
declare function absolutiseFoirMediaUrl(url: string | null | undefined, cdnBase?: string): string | undefined;
|
|
48
|
+
interface FoirGraphQLErrorItem {
|
|
49
|
+
message: string;
|
|
50
|
+
path?: Array<string | number>;
|
|
51
|
+
}
|
|
52
|
+
declare class FoirRequestError extends Error {
|
|
53
|
+
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
54
|
+
readonly status?: number;
|
|
55
|
+
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
56
|
+
readonly graphQLErrors?: ReadonlyArray<FoirGraphQLErrorItem>;
|
|
57
|
+
readonly cause?: unknown;
|
|
58
|
+
constructor(message: string, options?: {
|
|
59
|
+
cause?: unknown;
|
|
60
|
+
status?: number;
|
|
61
|
+
graphQLErrors?: ReadonlyArray<FoirGraphQLErrorItem>;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Execute a typed GraphQL document against the Foir platform.
|
|
66
|
+
*
|
|
67
|
+
* Throws `FoirRequestError` on HTTP failure or GraphQL errors so the caller
|
|
68
|
+
* can decide whether to rethrow or fall back to local defaults. GraphQL can
|
|
69
|
+
* return partial data alongside errors; when data is present the errors are
|
|
70
|
+
* logged and the data returned (callers handle nulls), and we only throw when
|
|
71
|
+
* there's no data at all.
|
|
72
|
+
*/
|
|
73
|
+
interface FoirRequestOptions {
|
|
74
|
+
/**
|
|
75
|
+
* Customer bearer token. api-public still needs the `x-api-key` to resolve
|
|
76
|
+
* the project; the bearer adds the customer identity for self-scoped reads
|
|
77
|
+
* (e.g. `myProfile`). Used by `@foir/sdk/auth`'s post-login profile fetch.
|
|
78
|
+
*/
|
|
79
|
+
bearerToken?: string;
|
|
80
|
+
}
|
|
81
|
+
declare function foirRequest<TData, TVariables>(env: FoirEnv, document: TypedDocumentNode<TData, TVariables>, variables?: TVariables, options?: FoirRequestOptions): Promise<TData>;
|
|
82
|
+
|
|
83
|
+
export { FOIR_CDN_BASE, type FoirEnv, FoirRequestError, type FoirRequestOptions, absolutiseFoirMediaUrl, foirRequest, isFoirPreview };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/graphql/foir-request.ts
|
|
2
|
+
import { print } from "graphql";
|
|
3
|
+
function isFoirPreview(env) {
|
|
4
|
+
return env.FOIR_PREVIEW === "true";
|
|
5
|
+
}
|
|
6
|
+
var FOIR_CDN_BASE = "https://cdn.foir.dev/";
|
|
7
|
+
function absolutiseFoirMediaUrl(url, cdnBase = FOIR_CDN_BASE) {
|
|
8
|
+
if (!url) return void 0;
|
|
9
|
+
if (/^https?:\/\//i.test(url)) return url;
|
|
10
|
+
return cdnBase + url.replace(/^\/+/, "");
|
|
11
|
+
}
|
|
12
|
+
var FoirRequestError = class extends Error {
|
|
13
|
+
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
14
|
+
status;
|
|
15
|
+
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
16
|
+
graphQLErrors;
|
|
17
|
+
cause;
|
|
18
|
+
constructor(message, options) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "FoirRequestError";
|
|
21
|
+
this.cause = options?.cause;
|
|
22
|
+
this.status = options?.status;
|
|
23
|
+
this.graphQLErrors = options?.graphQLErrors;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
async function foirRequest(env, document, variables, options) {
|
|
27
|
+
const endpoint = env.PLATFORM_API_URL;
|
|
28
|
+
const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
|
|
29
|
+
if (!endpoint || !apiKey) {
|
|
30
|
+
throw new FoirRequestError(
|
|
31
|
+
"[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const headers = {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
"x-api-key": apiKey
|
|
37
|
+
};
|
|
38
|
+
if (isFoirPreview(env)) {
|
|
39
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
40
|
+
}
|
|
41
|
+
if (options?.bearerToken) {
|
|
42
|
+
headers["Authorization"] = `Bearer ${options.bearerToken}`;
|
|
43
|
+
}
|
|
44
|
+
let res;
|
|
45
|
+
try {
|
|
46
|
+
res = await fetch(endpoint, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers,
|
|
49
|
+
body: JSON.stringify({ query: print(document), variables })
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new FoirRequestError("[foir] network error", { cause: err });
|
|
53
|
+
}
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
|
|
56
|
+
status: res.status
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const json = await res.json();
|
|
60
|
+
if (json.errors?.length) {
|
|
61
|
+
const msg = json.errors.map((e) => e.message).join("; ");
|
|
62
|
+
if (json.data) {
|
|
63
|
+
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
64
|
+
} else {
|
|
65
|
+
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
66
|
+
graphQLErrors: json.errors
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!json.data) {
|
|
71
|
+
throw new FoirRequestError("[foir] empty GraphQL response");
|
|
72
|
+
}
|
|
73
|
+
return json.data;
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
FOIR_CDN_BASE,
|
|
77
|
+
FoirRequestError,
|
|
78
|
+
absolutiseFoirMediaUrl,
|
|
79
|
+
foirRequest,
|
|
80
|
+
isFoirPreview
|
|
81
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { GraphQLSchema } from 'graphql';
|
|
2
|
+
import { B as BlockPluginConfig } from '../shared-DkWlEd2Z.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@foir/sdk/codegen/block-registry` — a graphql-codegen plugin.
|
|
6
|
+
*
|
|
7
|
+
* Introspects the project's public schema, collects every member of the block
|
|
8
|
+
* union(s), and emits an EXHAUSTIVE `BlockRegistry` type — one key per block
|
|
9
|
+
* `__typename`, each typed `BlockComponent<ThatBlock, Ctx>`. Because the type
|
|
10
|
+
* requires every key, a project that adds an inline model (→ a new block
|
|
11
|
+
* `__typename`) breaks the consumer's build until the new component is wired:
|
|
12
|
+
* exhaustiveness by construction, replacing the hand-synced switch the
|
|
13
|
+
* storefront's `BlockDigester` used to carry.
|
|
14
|
+
*
|
|
15
|
+
* Wired automatically by `foirCodegen()`. The generated file imports the block
|
|
16
|
+
* types from the codegen output (`./graphql`) and `BlockComponent` from
|
|
17
|
+
* `@foir/sdk/content`, so it pairs directly with `createContentRenderer`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface BlockRegistryConfig extends BlockPluginConfig {
|
|
21
|
+
/** Name of the generated registry type. Default `BlockRegistry`. */
|
|
22
|
+
registryName?: string;
|
|
23
|
+
}
|
|
24
|
+
/** graphql-codegen plugin entrypoint. */
|
|
25
|
+
declare const plugin: (schema: GraphQLSchema, _documents: unknown, config?: BlockRegistryConfig) => string;
|
|
26
|
+
|
|
27
|
+
export { type BlockRegistryConfig, plugin };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_GRAPHQL_IMPORT,
|
|
3
|
+
DEFAULT_SDK_IMPORT,
|
|
4
|
+
DEFAULT_UNIONS,
|
|
5
|
+
collectBlockMembers
|
|
6
|
+
} from "../chunk-ADO5FJNM.js";
|
|
7
|
+
|
|
8
|
+
// src/codegen/block-registry.ts
|
|
9
|
+
var plugin = (schema, _documents, config = {}) => {
|
|
10
|
+
const members = collectBlockMembers(schema, config.unions ?? DEFAULT_UNIONS);
|
|
11
|
+
const graphqlImport = config.graphqlImport ?? DEFAULT_GRAPHQL_IMPORT;
|
|
12
|
+
const sdkImport = config.sdkImport ?? DEFAULT_SDK_IMPORT;
|
|
13
|
+
const registryName = config.registryName ?? "BlockRegistry";
|
|
14
|
+
return [
|
|
15
|
+
"/* eslint-disable */",
|
|
16
|
+
"// Generated by @foir/sdk/codegen/block-registry. Do not edit by hand.",
|
|
17
|
+
"// Regenerated on every codegen run \u2014 omitting a block type is a compile error.",
|
|
18
|
+
`import type { BlockComponent } from '${sdkImport}';`,
|
|
19
|
+
`import type {`,
|
|
20
|
+
...members.map((m) => ` ${m},`),
|
|
21
|
+
`} from '${graphqlImport}';`,
|
|
22
|
+
"",
|
|
23
|
+
`/**`,
|
|
24
|
+
` * Exhaustive map of every block \`__typename\` in the project schema to the`,
|
|
25
|
+
` * component that renders it. Pass an instance to \`createContentRenderer\`.`,
|
|
26
|
+
` */`,
|
|
27
|
+
`export type ${registryName}<Ctx = unknown> = {`,
|
|
28
|
+
...members.map((m) => ` ${m}: BlockComponent<${m}, Ctx>;`),
|
|
29
|
+
`};`,
|
|
30
|
+
""
|
|
31
|
+
].join("\n");
|
|
32
|
+
};
|
|
33
|
+
export {
|
|
34
|
+
plugin
|
|
35
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GraphQLSchema } from 'graphql';
|
|
2
|
+
import { B as BlockPluginConfig } from '../shared-DkWlEd2Z.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@foir/sdk/codegen/block-scaffold` — a one-shot graphql-codegen plugin.
|
|
6
|
+
*
|
|
7
|
+
* Emits a ContentRenderer shell: a stub component per block `__typename`, a
|
|
8
|
+
* `blockRegistry` of the (exhaustive) `BlockRegistry` type, and a
|
|
9
|
+
* `createContentRenderer` wiring. Unlike the block-registry plugin (which
|
|
10
|
+
* regenerates every run), this is a STARTING POINT the dev then owns — run it
|
|
11
|
+
* once via `foirScaffold()`, then implement each stub. It compiles out of the
|
|
12
|
+
* box (each stub is a typed no-op) so the build stays green while you fill them
|
|
13
|
+
* in. Adding a new block type later surfaces as a compile error on
|
|
14
|
+
* `blockRegistry` (via the regenerated `BlockRegistry` type), not here.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface BlockScaffoldConfig extends BlockPluginConfig {
|
|
18
|
+
/** Import path for the generated registry type. Default `./block-registry`. */
|
|
19
|
+
registryImport?: string;
|
|
20
|
+
/** Render-context type name threaded to every component. Default `unknown`. */
|
|
21
|
+
ctxType?: string;
|
|
22
|
+
/** Optional import line for the ctx type, e.g. `import type { Ctx } from '~/ctx'`. */
|
|
23
|
+
ctxImport?: string;
|
|
24
|
+
}
|
|
25
|
+
/** graphql-codegen plugin entrypoint. */
|
|
26
|
+
declare const plugin: (schema: GraphQLSchema, _documents: unknown, config?: BlockScaffoldConfig) => string;
|
|
27
|
+
|
|
28
|
+
export { type BlockScaffoldConfig, plugin };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_GRAPHQL_IMPORT,
|
|
3
|
+
DEFAULT_SDK_IMPORT,
|
|
4
|
+
DEFAULT_UNIONS,
|
|
5
|
+
collectBlockMembers
|
|
6
|
+
} from "../chunk-ADO5FJNM.js";
|
|
7
|
+
|
|
8
|
+
// src/codegen/block-scaffold.ts
|
|
9
|
+
var plugin = (schema, _documents, config = {}) => {
|
|
10
|
+
const members = collectBlockMembers(schema, config.unions ?? DEFAULT_UNIONS);
|
|
11
|
+
const graphqlImport = config.graphqlImport ?? DEFAULT_GRAPHQL_IMPORT;
|
|
12
|
+
const sdkImport = config.sdkImport ?? DEFAULT_SDK_IMPORT;
|
|
13
|
+
const registryImport = config.registryImport ?? "./block-registry";
|
|
14
|
+
const ctx = config.ctxType ?? "unknown";
|
|
15
|
+
const stub = (m) => [
|
|
16
|
+
`function ${m}View(props: BlockComponentProps<${m}, ${ctx}>) {`,
|
|
17
|
+
` // TODO: render ${m} from props.block (props.ctx, props.priority available)`,
|
|
18
|
+
` void props;`,
|
|
19
|
+
` return null;`,
|
|
20
|
+
`}`
|
|
21
|
+
].join("\n");
|
|
22
|
+
return [
|
|
23
|
+
"/* eslint-disable */",
|
|
24
|
+
"// Scaffolded once by @foir/sdk/codegen/block-scaffold \u2014 you own this file.",
|
|
25
|
+
"// Implement each stub; the build stays green until you do.",
|
|
26
|
+
`import { createContentRenderer, type BlockComponentProps } from '${sdkImport}';`,
|
|
27
|
+
`import type { BlockRegistry } from '${registryImport}';`,
|
|
28
|
+
`import type {`,
|
|
29
|
+
...members.map((m) => ` ${m},`),
|
|
30
|
+
`} from '${graphqlImport}';`,
|
|
31
|
+
...config.ctxImport ? ["", config.ctxImport] : [],
|
|
32
|
+
"",
|
|
33
|
+
...members.map(stub),
|
|
34
|
+
"",
|
|
35
|
+
`export const blockRegistry: BlockRegistry<${ctx}> = {`,
|
|
36
|
+
...members.map((m) => ` ${m}: ${m}View,`),
|
|
37
|
+
`};`,
|
|
38
|
+
"",
|
|
39
|
+
`export const ContentRenderer = createContentRenderer<${ctx}>(blockRegistry);`,
|
|
40
|
+
""
|
|
41
|
+
].join("\n");
|
|
42
|
+
};
|
|
43
|
+
export {
|
|
44
|
+
plugin
|
|
45
|
+
};
|