@fakeware/core 0.0.6 → 0.0.8

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.
@@ -1,158 +1,2 @@
1
- import { z } from "zod";
2
- import { ApiClientError, createAdminAPIClient } from "@shopware/api-client";
3
- //#region src/shopware/client.ts
4
- const REQUEST_TIMEOUT_MS = 12e4;
5
- function createShopwareClient(connection) {
6
- return createAdminAPIClient({
7
- baseURL: `${connection.url.replace(/\/$/, "")}/api`,
8
- credentials: {
9
- grant_type: "client_credentials",
10
- client_id: connection.clientId,
11
- client_secret: connection.clientSecret
12
- },
13
- fetchOptions: { timeout: REQUEST_TIMEOUT_MS }
14
- });
15
- }
16
- //#endregion
17
- //#region src/shopware/errors.ts
18
- var ShopwareConnectionError = class extends Error {};
19
- //#endregion
20
- //#region src/shopware/locale.ts
21
- const SYSTEM_LANGUAGE_ID = "2fbb5fe2e29a4d70aa5854ce7ce3e20b";
22
- const languageRowSchema = z.object({
23
- id: z.string(),
24
- locale: z.object({ code: z.string().optional() }).nullish()
25
- });
26
- function parseLanguageRows(rows) {
27
- const result = z.array(languageRowSchema).safeParse(rows);
28
- if (!result.success) throw new ShopwareConnectionError("Shopware returned an unexpected response shape for languages.");
29
- return result.data;
30
- }
31
- function toShopInfo(rows) {
32
- const seen = /* @__PURE__ */ new Set();
33
- const locales = [];
34
- let systemLocale;
35
- for (const row of rows) {
36
- const code = row.locale?.code;
37
- if (!code) continue;
38
- if (row.id === SYSTEM_LANGUAGE_ID) systemLocale = code;
39
- if (!seen.has(code)) {
40
- seen.add(code);
41
- locales.push(code);
42
- }
43
- }
44
- if (locales.length === 0) throw new ShopwareConnectionError("Shopware returned no usable locales.");
45
- return {
46
- locales,
47
- defaultLocale: systemLocale ?? locales[0]
48
- };
49
- }
50
- //#endregion
51
- //#region src/shopware/operations.ts
52
- function safeJsonParse(input) {
53
- try {
54
- return JSON.parse(input);
55
- } catch {
56
- return null;
57
- }
58
- }
59
- function isTimeoutError(error) {
60
- let current = error;
61
- while (current instanceof Error) {
62
- if (current.name === "TimeoutError") return true;
63
- current = current.cause;
64
- }
65
- return false;
66
- }
67
- function missingPrivileges(error) {
68
- for (const e of error.details.errors) {
69
- if (e.code !== "FRAMEWORK__MISSING_PRIVILEGE_ERROR" || !e.detail) continue;
70
- const parsed = safeJsonParse(e.detail);
71
- if (parsed?.missingPrivileges?.length) return parsed.missingPrivileges;
72
- }
73
- return [];
74
- }
75
- function validationMessages(error) {
76
- return error.details.errors.map((e) => {
77
- const field = e.source?.pointer?.replace(/^\/\d+\/\d+\//, "");
78
- const detail = e.detail ?? e.title ?? "Invalid value.";
79
- return field ? `${field}: ${detail}` : detail;
80
- }).filter((message, index, all) => all.indexOf(message) === index);
81
- }
82
- function toConnectionError(connection, error) {
83
- if (isTimeoutError(error)) return new ShopwareConnectionError(`${connection.url} did not respond within ${REQUEST_TIMEOUT_MS / 1e3}s, the shop may be slow or unreachable.`);
84
- if (error instanceof ApiClientError) switch (error.status) {
85
- case 400: {
86
- const messages = validationMessages(error);
87
- return new ShopwareConnectionError(messages.length ? `Shopware rejected the data — ${messages.join(" ")}` : `Shopware rejected the request (HTTP 400) from ${connection.url}.`);
88
- }
89
- case 401: return new ShopwareConnectionError("Authentication failed — check the client ID and client secret of your integration.");
90
- case 403: {
91
- const missing = missingPrivileges(error);
92
- if (missing.length) return new ShopwareConnectionError(`The integration is missing the ${missing.join(", ")} ${missing.length === 1 ? "privilege" : "privileges"} — grant them to its role in Settings → System → Integrations.`);
93
- return new ShopwareConnectionError("The integration is missing permissions — grant its role admin API access in Settings → System → Integrations.");
94
- }
95
- case 404: return new ShopwareConnectionError(`No Shopware admin API found at ${connection.url} — check the shop URL.`);
96
- default:
97
- if (error.status >= 500) return new ShopwareConnectionError(`${connection.url} is not responding (HTTP ${error.status}) — the shop may be down or in maintenance.`);
98
- return new ShopwareConnectionError(`Shopware returned an unexpected response (HTTP ${error.status}) from ${connection.url}.`);
99
- }
100
- return new ShopwareConnectionError(`Could not reach ${connection.url} — check the URL and your network connection.`);
101
- }
102
- async function validateConnection(connection) {
103
- const client = createShopwareClient(connection);
104
- try {
105
- await client.invoke("infoShopwareVersion get /_info/version");
106
- } catch (error) {
107
- throw toConnectionError(connection, error);
108
- }
109
- }
110
- async function fetchShopInfo(connection) {
111
- const client = createShopwareClient(connection);
112
- try {
113
- const { data } = await client.invoke("searchLanguage post /search/language", { body: {
114
- associations: { locale: {} },
115
- limit: 500
116
- } });
117
- return toShopInfo(parseLanguageRows(data.data ?? []));
118
- } catch (error) {
119
- throw toConnectionError(connection, error);
120
- }
121
- }
122
- //#endregion
123
- //#region src/shopware/sink.ts
124
- const SYNC_BATCH_SIZE = 50;
125
- function chunk(items, size) {
126
- const out = [];
127
- for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
128
- return out;
129
- }
130
- function createSyncSink(connection, options = {}) {
131
- const client = options.client ?? createShopwareClient(connection);
132
- async function sync(entity, action, payload) {
133
- for (const batch of chunk(payload, SYNC_BATCH_SIZE)) try {
134
- await client.invoke("sync post /_action/sync", {
135
- headers: { "indexing-behavior": "use-queue-indexing" },
136
- body: [{
137
- entity,
138
- action,
139
- payload: batch
140
- }]
141
- });
142
- } catch (error) {
143
- throw toConnectionError(connection, error);
144
- }
145
- }
146
- return {
147
- async upsert(entity, records) {
148
- if (records.length > 0) await sync(entity, "upsert", records);
149
- },
150
- async delete(entity, ids) {
151
- if (ids.length > 0) await sync(entity, "delete", ids.map((id) => ({ id })));
152
- }
153
- };
154
- }
155
- //#endregion
156
- export { ShopwareConnectionError, createShopwareClient, createSyncSink, fetchShopInfo, toConnectionError, validateConnection };
157
-
158
- //# sourceMappingURL=index.mjs.map
1
+ import { a as toConnectionError, c as createShopwareClient, i as fetchShopInfo, n as createSyncSink, o as validateConnection, r as estimateSyncBytes, s as ShopwareConnectionError, t as ATOMIC_REQUEST_BYTE_LIMIT } from "../shopware-CIZF8Nuo.mjs";
2
+ export { ATOMIC_REQUEST_BYTE_LIMIT, ShopwareConnectionError, createShopwareClient, createSyncSink, estimateSyncBytes, fetchShopInfo, toConnectionError, validateConnection };
@@ -0,0 +1,204 @@
1
+ import { z } from "zod";
2
+ import { ApiClientError, createAdminAPIClient } from "@shopware/api-client";
3
+ //#region src/shopware/client.ts
4
+ const REQUEST_TIMEOUT_MS = 12e4;
5
+ function createShopwareClient(connection) {
6
+ return createAdminAPIClient({
7
+ baseURL: `${connection.url.replace(/\/$/, "")}/api`,
8
+ credentials: {
9
+ grant_type: "client_credentials",
10
+ client_id: connection.clientId,
11
+ client_secret: connection.clientSecret
12
+ },
13
+ fetchOptions: { timeout: REQUEST_TIMEOUT_MS }
14
+ });
15
+ }
16
+ //#endregion
17
+ //#region src/shopware/errors.ts
18
+ var ShopwareConnectionError = class extends Error {};
19
+ //#endregion
20
+ //#region src/shopware/locale.ts
21
+ const SYSTEM_LANGUAGE_ID = "2fbb5fe2e29a4d70aa5854ce7ce3e20b";
22
+ const languageRowSchema = z.object({
23
+ id: z.string(),
24
+ locale: z.object({ code: z.string().optional() }).nullish()
25
+ });
26
+ function parseLanguageRows(rows) {
27
+ const result = z.array(languageRowSchema).safeParse(rows);
28
+ if (!result.success) throw new ShopwareConnectionError("Shopware returned an unexpected response shape for languages.");
29
+ return result.data;
30
+ }
31
+ function toShopInfo(rows) {
32
+ const seen = /* @__PURE__ */ new Set();
33
+ const locales = [];
34
+ let systemLocale;
35
+ for (const row of rows) {
36
+ const code = row.locale?.code;
37
+ if (!code) continue;
38
+ if (row.id === SYSTEM_LANGUAGE_ID) systemLocale = code;
39
+ if (!seen.has(code)) {
40
+ seen.add(code);
41
+ locales.push(code);
42
+ }
43
+ }
44
+ if (locales.length === 0) throw new ShopwareConnectionError("Shopware returned no usable locales.");
45
+ return {
46
+ locales,
47
+ defaultLocale: systemLocale ?? locales[0]
48
+ };
49
+ }
50
+ //#endregion
51
+ //#region src/shopware/operations.ts
52
+ function safeJsonParse(input) {
53
+ try {
54
+ return JSON.parse(input);
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function isTimeoutError(error) {
60
+ let current = error;
61
+ while (current instanceof Error) {
62
+ if (current.name === "TimeoutError") return true;
63
+ current = current.cause;
64
+ }
65
+ return false;
66
+ }
67
+ function missingPrivileges(error) {
68
+ for (const e of error.details.errors) {
69
+ if (e.code !== "FRAMEWORK__MISSING_PRIVILEGE_ERROR" || !e.detail) continue;
70
+ const parsed = safeJsonParse(e.detail);
71
+ if (parsed?.missingPrivileges?.length) return parsed.missingPrivileges;
72
+ }
73
+ return [];
74
+ }
75
+ function fieldName(pointer) {
76
+ if (!pointer) return null;
77
+ const segments = pointer.split("/").filter((s) => s !== "" && !/^\d+$/.test(s));
78
+ return segments.length ? segments[segments.length - 1] ?? null : null;
79
+ }
80
+ function validationMessages(error) {
81
+ return error.details.errors.map((e) => {
82
+ const field = fieldName(e.source?.pointer);
83
+ const detail = e.detail ?? e.title ?? "Invalid value.";
84
+ return field ? `${field}: ${detail}` : detail;
85
+ }).filter((message, index, all) => all.indexOf(message) === index);
86
+ }
87
+ function toConnectionError(connection, error) {
88
+ if (isTimeoutError(error)) return new ShopwareConnectionError(`${connection.url} did not respond within ${REQUEST_TIMEOUT_MS / 1e3}s, the shop may be slow or unreachable.`);
89
+ if (error instanceof ApiClientError) switch (error.status) {
90
+ case 400: {
91
+ const messages = validationMessages(error);
92
+ if (!messages.length) return new ShopwareConnectionError(`Shopware rejected the request (HTTP 400) from ${connection.url}.`);
93
+ const shown = messages.slice(0, 5);
94
+ const more = messages.length - shown.length;
95
+ return new ShopwareConnectionError(`Shopware rejected the data:\n${shown.map((m) => ` - ${m}`).join("\n")}${more > 0 ? `\n - …and ${more} more` : ""}`);
96
+ }
97
+ case 401: return new ShopwareConnectionError("Authentication failed — check the client ID and client secret of your integration.");
98
+ case 403: {
99
+ const missing = missingPrivileges(error);
100
+ if (missing.length) return new ShopwareConnectionError(`The integration is missing the ${missing.join(", ")} ${missing.length === 1 ? "privilege" : "privileges"} — grant them to its role in Settings → System → Integrations.`);
101
+ return new ShopwareConnectionError("The integration is missing permissions — grant its role admin API access in Settings → System → Integrations.");
102
+ }
103
+ case 404: return new ShopwareConnectionError(`No Shopware admin API found at ${connection.url} — check the shop URL.`);
104
+ default:
105
+ if (error.status >= 500) return new ShopwareConnectionError(`${connection.url} is not responding (HTTP ${error.status}) — the shop may be down or in maintenance.`);
106
+ return new ShopwareConnectionError(`Shopware returned an unexpected response (HTTP ${error.status}) from ${connection.url}.`);
107
+ }
108
+ return new ShopwareConnectionError(`Could not reach ${connection.url} — check the URL and your network connection.`);
109
+ }
110
+ async function validateConnection(connection) {
111
+ const client = createShopwareClient(connection);
112
+ try {
113
+ await client.invoke("infoShopwareVersion get /_info/version");
114
+ } catch (error) {
115
+ throw toConnectionError(connection, error);
116
+ }
117
+ }
118
+ async function fetchShopInfo(connection) {
119
+ const client = createShopwareClient(connection);
120
+ try {
121
+ const { data } = await client.invoke("searchLanguage post /search/language", { body: {
122
+ associations: { locale: {} },
123
+ limit: 500
124
+ } });
125
+ return toShopInfo(parseLanguageRows(data.data ?? []));
126
+ } catch (error) {
127
+ throw toConnectionError(connection, error);
128
+ }
129
+ }
130
+ //#endregion
131
+ //#region src/shopware/sink.ts
132
+ const SYNC_BATCH_SIZE = 50;
133
+ const ATOMIC_REQUEST_BYTE_LIMIT = 5 * 1024 * 1024;
134
+ function toSyncBody(operations) {
135
+ return operations.map((op) => op.action === "upsert" ? {
136
+ entity: op.entity,
137
+ action: "upsert",
138
+ payload: op.records
139
+ } : {
140
+ entity: op.entity,
141
+ action: "delete",
142
+ payload: op.ids.map((id) => ({ id }))
143
+ });
144
+ }
145
+ function estimateSyncBytes(operations) {
146
+ return Buffer.byteLength(JSON.stringify(toSyncBody(operations)), "utf8");
147
+ }
148
+ function chunk(items, size) {
149
+ const out = [];
150
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
151
+ return out;
152
+ }
153
+ function createSyncSink(connection, options = {}) {
154
+ const client = options.client ?? createShopwareClient(connection);
155
+ async function sync(entity, action, payload, onBatch) {
156
+ const batches = chunk(payload, SYNC_BATCH_SIZE);
157
+ let records = 0;
158
+ for (const [i, batch] of batches.entries()) {
159
+ try {
160
+ await client.invoke("sync post /_action/sync", {
161
+ headers: { "indexing-behavior": "use-queue-indexing" },
162
+ body: [{
163
+ entity,
164
+ action,
165
+ payload: batch
166
+ }]
167
+ });
168
+ } catch (error) {
169
+ throw toConnectionError(connection, error);
170
+ }
171
+ records += batch.length;
172
+ onBatch?.({
173
+ records,
174
+ recordsTotal: payload.length,
175
+ batches: i + 1,
176
+ batchesTotal: batches.length
177
+ });
178
+ }
179
+ }
180
+ return {
181
+ async upsert(entity, records, onBatch) {
182
+ if (records.length > 0) await sync(entity, "upsert", records, onBatch);
183
+ },
184
+ async delete(entity, ids, onBatch) {
185
+ if (ids.length > 0) await sync(entity, "delete", ids.map((id) => ({ id })), onBatch);
186
+ },
187
+ async applyAtomic(operations) {
188
+ const body = toSyncBody(operations).filter((op) => op.payload.length > 0);
189
+ if (body.length === 0) return;
190
+ try {
191
+ await client.invoke("sync post /_action/sync", {
192
+ headers: { "indexing-behavior": "use-queue-indexing" },
193
+ body
194
+ });
195
+ } catch (error) {
196
+ throw toConnectionError(connection, error);
197
+ }
198
+ }
199
+ };
200
+ }
201
+ //#endregion
202
+ export { toConnectionError as a, createShopwareClient as c, fetchShopInfo as i, createSyncSink as n, validateConnection as o, estimateSyncBytes as r, ShopwareConnectionError as s, ATOMIC_REQUEST_BYTE_LIMIT as t };
203
+
204
+ //# sourceMappingURL=shopware-CIZF8Nuo.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shopware-CIZF8Nuo.mjs","names":[],"sources":["../src/shopware/client.ts","../src/shopware/errors.ts","../src/shopware/locale.ts","../src/shopware/operations.ts","../src/shopware/sink.ts"],"sourcesContent":["import { createAdminAPIClient } from '@shopware/api-client'\nimport type { operations } from '@shopware/api-client/admin-api-types'\nimport type { ShopwareConnection } from './types'\n\nexport type ShopwareClient = ReturnType<typeof createAdminAPIClient<operations>>\n\nexport const REQUEST_TIMEOUT_MS = 120_000\n\nexport function createShopwareClient(connection: ShopwareConnection): ShopwareClient {\n return createAdminAPIClient<operations>({\n baseURL: `${connection.url.replace(/\\/$/, '')}/api`,\n credentials: {\n grant_type: 'client_credentials',\n client_id: connection.clientId,\n client_secret: connection.clientSecret,\n },\n fetchOptions: {\n timeout: REQUEST_TIMEOUT_MS,\n },\n })\n}\n","export class ShopwareConnectionError extends Error {}\n","import { z } from 'zod'\nimport { ShopwareConnectionError } from './errors'\nimport type { ShopInfo } from './types'\n\nconst SYSTEM_LANGUAGE_ID = '2fbb5fe2e29a4d70aa5854ce7ce3e20b'\n\nconst languageRowSchema = z.object({\n id: z.string(),\n locale: z.object({ code: z.string().optional() }).nullish(),\n})\n\nexport type LanguageRow = z.infer<typeof languageRowSchema>\n\nexport function parseLanguageRows(rows: unknown): LanguageRow[] {\n const result = z.array(languageRowSchema).safeParse(rows)\n if (!result.success) {\n throw new ShopwareConnectionError(\n 'Shopware returned an unexpected response shape for languages.',\n )\n }\n return result.data\n}\n\nexport function toShopInfo(rows: LanguageRow[]): ShopInfo {\n const seen = new Set<string>()\n const locales: string[] = []\n let systemLocale: string | undefined\n\n for (const row of rows) {\n const code = row.locale?.code\n if (!code) continue\n if (row.id === SYSTEM_LANGUAGE_ID) systemLocale = code\n if (!seen.has(code)) {\n seen.add(code)\n locales.push(code)\n }\n }\n\n if (locales.length === 0) {\n throw new ShopwareConnectionError('Shopware returned no usable locales.')\n }\n\n return { locales, defaultLocale: systemLocale ?? (locales[0] as string) }\n}\n","import { ApiClientError, type ApiError } from '@shopware/api-client'\nimport { createShopwareClient, REQUEST_TIMEOUT_MS } from './client'\nimport { ShopwareConnectionError } from './errors'\nimport { parseLanguageRows, toShopInfo } from './locale'\nimport type { ShopInfo, ShopwareConnection } from './types'\n\nfunction safeJsonParse<T>(input: string): T | null {\n try {\n return JSON.parse(input) as T\n } catch {\n return null\n }\n}\n\nfunction isTimeoutError(error: unknown): boolean {\n let current: unknown = error\n while (current instanceof Error) {\n if (current.name === 'TimeoutError') return true\n current = current.cause\n }\n return false\n}\n\nfunction missingPrivileges(error: ApiClientError<{ errors: ApiError[] }>): string[] {\n for (const e of error.details.errors) {\n if (e.code !== 'FRAMEWORK__MISSING_PRIVILEGE_ERROR' || !e.detail) continue\n const parsed = safeJsonParse<{ missingPrivileges?: string[] }>(e.detail)\n if (parsed?.missingPrivileges?.length) return parsed.missingPrivileges\n }\n return []\n}\n\nfunction fieldName(pointer: string | undefined): string | null {\n if (!pointer) return null\n const segments = pointer.split('/').filter((s) => s !== '' && !/^\\d+$/.test(s))\n return segments.length ? (segments[segments.length - 1] ?? null) : null\n}\n\nfunction validationMessages(error: ApiClientError<{ errors: ApiError[] }>): string[] {\n return error.details.errors\n .map((e) => {\n const field = fieldName(e.source?.pointer)\n const detail = e.detail ?? e.title ?? 'Invalid value.'\n return field ? `${field}: ${detail}` : detail\n })\n .filter((message, index, all) => all.indexOf(message) === index)\n}\n\nexport function toConnectionError(\n connection: ShopwareConnection,\n error: unknown,\n): ShopwareConnectionError {\n if (isTimeoutError(error)) {\n return new ShopwareConnectionError(\n `${connection.url} did not respond within ${REQUEST_TIMEOUT_MS / 1000}s, the shop may be slow or unreachable.`,\n )\n }\n if (error instanceof ApiClientError) {\n switch (error.status) {\n case 400: {\n const messages = validationMessages(error)\n if (!messages.length) {\n return new ShopwareConnectionError(\n `Shopware rejected the request (HTTP 400) from ${connection.url}.`,\n )\n }\n const shown = messages.slice(0, 5)\n const more = messages.length - shown.length\n const list = shown.map((m) => ` - ${m}`).join('\\n')\n const tail = more > 0 ? `\\n - …and ${more} more` : ''\n return new ShopwareConnectionError(`Shopware rejected the data:\\n${list}${tail}`)\n }\n case 401:\n return new ShopwareConnectionError(\n 'Authentication failed — check the client ID and client secret of your integration.',\n )\n case 403: {\n const missing = missingPrivileges(error)\n if (missing.length) {\n return new ShopwareConnectionError(\n `The integration is missing the ${missing.join(', ')} ${missing.length === 1 ? 'privilege' : 'privileges'} — grant them to its role in Settings → System → Integrations.`,\n )\n }\n return new ShopwareConnectionError(\n 'The integration is missing permissions — grant its role admin API access in Settings → System → Integrations.',\n )\n }\n case 404:\n return new ShopwareConnectionError(\n `No Shopware admin API found at ${connection.url} — check the shop URL.`,\n )\n default:\n if (error.status >= 500) {\n return new ShopwareConnectionError(\n `${connection.url} is not responding (HTTP ${error.status}) — the shop may be down or in maintenance.`,\n )\n }\n return new ShopwareConnectionError(\n `Shopware returned an unexpected response (HTTP ${error.status}) from ${connection.url}.`,\n )\n }\n }\n return new ShopwareConnectionError(\n `Could not reach ${connection.url} — check the URL and your network connection.`,\n )\n}\n\nexport async function validateConnection(connection: ShopwareConnection): Promise<void> {\n const client = createShopwareClient(connection)\n try {\n await client.invoke('infoShopwareVersion get /_info/version')\n } catch (error) {\n throw toConnectionError(connection, error)\n }\n}\n\nexport async function fetchShopInfo(connection: ShopwareConnection): Promise<ShopInfo> {\n const client = createShopwareClient(connection)\n try {\n const { data } = await client.invoke('searchLanguage post /search/language', {\n body: { associations: { locale: {} }, limit: 500 },\n })\n return toShopInfo(parseLanguageRows(data.data ?? []))\n } catch (error) {\n throw toConnectionError(connection, error)\n }\n}\n","import type { OnBatch, ShopwareSink, SinkRecord, SyncOperation } from '../domain'\nimport { createShopwareClient, type ShopwareClient } from './client'\nimport { toConnectionError } from './operations'\nimport type { ShopwareConnection } from './types'\n\nexport interface SyncSinkOptions {\n client?: ShopwareClient\n}\n\nconst SYNC_BATCH_SIZE = 50\n\nexport const ATOMIC_REQUEST_BYTE_LIMIT = 5 * 1024 * 1024\n\ninterface SyncBodyEntry {\n entity: string\n action: 'upsert' | 'delete'\n payload: Record<string, unknown>[]\n}\n\nfunction toSyncBody(operations: SyncOperation[]): SyncBodyEntry[] {\n return operations.map((op) =>\n op.action === 'upsert'\n ? { entity: op.entity, action: 'upsert', payload: op.records }\n : { entity: op.entity, action: 'delete', payload: op.ids.map((id) => ({ id })) },\n )\n}\n\nexport function estimateSyncBytes(operations: SyncOperation[]): number {\n return Buffer.byteLength(JSON.stringify(toSyncBody(operations)), 'utf8')\n}\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\nexport function createSyncSink(\n connection: ShopwareConnection,\n options: SyncSinkOptions = {},\n): ShopwareSink {\n const client = options.client ?? createShopwareClient(connection)\n\n async function sync(\n entity: string,\n action: 'upsert' | 'delete',\n payload: Record<string, unknown>[],\n onBatch?: OnBatch,\n ): Promise<void> {\n const batches = chunk(payload, SYNC_BATCH_SIZE)\n let records = 0\n for (const [i, batch] of batches.entries()) {\n try {\n await client.invoke('sync post /_action/sync', {\n headers: { 'indexing-behavior': 'use-queue-indexing' },\n body: [{ entity, action, payload: batch as never }],\n })\n } catch (error) {\n throw toConnectionError(connection, error)\n }\n records += batch.length\n onBatch?.({\n records,\n recordsTotal: payload.length,\n batches: i + 1,\n batchesTotal: batches.length,\n })\n }\n }\n\n return {\n async upsert(entity: string, records: SinkRecord[], onBatch?: OnBatch): Promise<void> {\n if (records.length > 0) await sync(entity, 'upsert', records, onBatch)\n },\n async delete(entity: string, ids: string[], onBatch?: OnBatch): Promise<void> {\n if (ids.length > 0)\n await sync(\n entity,\n 'delete',\n ids.map((id) => ({ id })),\n onBatch,\n )\n },\n async applyAtomic(operations: SyncOperation[]): Promise<void> {\n const body = toSyncBody(operations).filter((op) => op.payload.length > 0)\n if (body.length === 0) return\n try {\n await client.invoke('sync post /_action/sync', {\n headers: { 'indexing-behavior': 'use-queue-indexing' },\n body: body as never,\n })\n } catch (error) {\n throw toConnectionError(connection, error)\n }\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fakeware/core",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Fakeware core library that is the base for @fakeware/cli",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,7 @@
20
20
  "dist"
21
21
  ],
22
22
  "exports": {
23
+ "./package.json": "./package.json",
23
24
  ".": {
24
25
  "types": "./dist/index.d.mts",
25
26
  "import": "./dist/index.mjs"
@@ -41,6 +42,7 @@
41
42
  },
42
43
  "dependencies": {
43
44
  "@shopware/api-client": "1.5.0",
45
+ "jiti": "2.7.0",
44
46
  "zod": "4.4.3"
45
47
  },
46
48
  "devDependencies": {
@@ -1,130 +0,0 @@
1
- import { access, readFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, join, resolve } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- import { z } from "zod";
5
- //#region src/runtime/load-module.ts
6
- const RUNTIME_TS_HELP = "fakeware needs to import your TypeScript files at runtime. Run it under Bun, or with Node >=22.6 (native type stripping), or via a TypeScript loader such as tsx.";
7
- var LoadModuleError = class extends Error {};
8
- function isBun() {
9
- return typeof globalThis.Bun !== "undefined";
10
- }
11
- function nodeStripsTypes() {
12
- const feature = process.features.typescript;
13
- return feature === "strip" || feature === "transform";
14
- }
15
- async function loadModule(absPath) {
16
- if (!isBun() && !nodeStripsTypes()) throw new LoadModuleError(RUNTIME_TS_HELP);
17
- try {
18
- return await import(pathToFileURL(absPath).href);
19
- } catch (error) {
20
- throw new LoadModuleError(`Could not load ${absPath}: ${error instanceof Error ? error.message : String(error)}`);
21
- }
22
- }
23
- //#endregion
24
- //#region src/config/define.ts
25
- function defineConfig(config) {
26
- return config;
27
- }
28
- //#endregion
29
- //#region src/config/errors.ts
30
- var ConfigError = class extends Error {};
31
- //#endregion
32
- //#region src/config/interpolate.ts
33
- const ENV_REF = /^\$([A-Z0-9_]+)$/;
34
- function interpolate(value, env) {
35
- if (typeof value === "string") {
36
- const match = ENV_REF.exec(value);
37
- if (!match) return value;
38
- const name = match[1];
39
- const resolved = env[name];
40
- if (resolved === void 0) throw new ConfigError(`Config references $${name}, but it is not set (check your .env).`);
41
- return resolved;
42
- }
43
- if (Array.isArray(value)) return value.map((item) => interpolate(item, env));
44
- if (value && typeof value === "object") {
45
- const out = {};
46
- for (const [k, v] of Object.entries(value)) out[k] = interpolate(v, env);
47
- return out;
48
- }
49
- return value;
50
- }
51
- //#endregion
52
- //#region src/config/schema.ts
53
- const shopwareSchema = z.object({
54
- url: z.string().min(1, "shopware.url is required"),
55
- clientId: z.string().min(1, "shopware.clientId is required"),
56
- clientSecret: z.string().min(1, "shopware.clientSecret is required")
57
- });
58
- const fakewareConfigSchema = z.object({ shopware: shopwareSchema.optional() });
59
- //#endregion
60
- //#region src/config/load.ts
61
- const DEFAULT_CONFIG_FILENAME = "fakeware.config.ts";
62
- async function fileExists(path) {
63
- try {
64
- await access(path);
65
- return true;
66
- } catch {
67
- return false;
68
- }
69
- }
70
- async function findConfig(cwd) {
71
- let dir = cwd;
72
- for (;;) {
73
- const candidate = join(dir, DEFAULT_CONFIG_FILENAME);
74
- if (await fileExists(candidate)) return candidate;
75
- const parent = dirname(dir);
76
- if (parent === dir) break;
77
- dir = parent;
78
- }
79
- throw new ConfigError(`No ${DEFAULT_CONFIG_FILENAME} found in ${cwd} or any parent directory. Run \`fakeware init\` first.`);
80
- }
81
- async function readEnvFile(projectRoot) {
82
- const path = join(projectRoot, ".env");
83
- if (!await fileExists(path)) return {};
84
- const out = {};
85
- const contents = await readFile(path, "utf8");
86
- for (const raw of contents.split("\n")) {
87
- const line = raw.trim();
88
- if (!line || line.startsWith("#")) continue;
89
- const eq = line.indexOf("=");
90
- if (eq === -1) continue;
91
- const key = line.slice(0, eq).trim();
92
- let val = line.slice(eq + 1).trim();
93
- if (val.startsWith("\"") && val.endsWith("\"") || val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
94
- out[key] = val;
95
- }
96
- return out;
97
- }
98
- function isConfigFn(value) {
99
- return typeof value === "function";
100
- }
101
- async function loadConfig(opts = {}) {
102
- const cwd = opts.cwd ?? process.cwd();
103
- const configPath = opts.configFile ? isAbsolute(opts.configFile) ? opts.configFile : resolve(cwd, opts.configFile) : await findConfig(cwd);
104
- const projectRoot = dirname(configPath);
105
- const env = {
106
- ...process.env,
107
- ...await readEnvFile(projectRoot)
108
- };
109
- const exported = (await loadModule(configPath)).default;
110
- if (exported === void 0) throw new ConfigError(`${configPath} must \`export default defineConfig(...)\`.`);
111
- const configEnv = {
112
- env,
113
- mode: opts.mode ?? "development"
114
- };
115
- const interpolated = interpolate(isConfigFn(exported) ? exported(configEnv) : exported, env);
116
- const parsed = fakewareConfigSchema.safeParse(interpolated);
117
- if (!parsed.success) throw new ConfigError(`Invalid config in ${configPath}: ${parsed.error.message}`);
118
- const { shopware } = parsed.data;
119
- if (!shopware) throw new ConfigError(`No \`shopware\` connection configured in ${configPath}. up/down need a shop to talk to.`);
120
- return {
121
- config: parsed.data,
122
- connection: shopware,
123
- configPath,
124
- projectRoot
125
- };
126
- }
127
- //#endregion
128
- export { interpolate as a, LoadModuleError as c, shopwareSchema as i, loadModule as l, loadConfig as n, ConfigError as o, fakewareConfigSchema as r, defineConfig as s, DEFAULT_CONFIG_FILENAME as t };
129
-
130
- //# sourceMappingURL=config-CSPA4Itj.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-CSPA4Itj.mjs","names":[],"sources":["../src/runtime/load-module.ts","../src/config/define.ts","../src/config/errors.ts","../src/config/interpolate.ts","../src/config/schema.ts","../src/config/load.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url'\n\nconst RUNTIME_TS_HELP =\n 'fakeware needs to import your TypeScript files at runtime. Run it under Bun, or with Node >=22.6 (native type stripping), or via a TypeScript loader such as tsx.'\n\nexport class LoadModuleError extends Error {}\n\nfunction isBun(): boolean {\n return typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'\n}\n\nfunction nodeStripsTypes(): boolean {\n const feature = (process.features as { typescript?: unknown }).typescript\n return feature === 'strip' || feature === 'transform'\n}\n\nexport async function loadModule<T = unknown>(absPath: string): Promise<T> {\n if (!isBun() && !nodeStripsTypes()) {\n throw new LoadModuleError(RUNTIME_TS_HELP)\n }\n try {\n return (await import(pathToFileURL(absPath).href)) as T\n } catch (error) {\n throw new LoadModuleError(\n `Could not load ${absPath}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n}\n","import type { FakewareUserConfig } from './schema'\n\nexport interface ConfigEnv {\n env: Record<string, string | undefined>\n mode: string\n}\n\nexport type FakewareConfigFn = (env: ConfigEnv) => FakewareUserConfig\n\nexport function defineConfig(config: FakewareUserConfig): FakewareUserConfig\nexport function defineConfig(config: FakewareConfigFn): FakewareConfigFn\nexport function defineConfig(\n config: FakewareUserConfig | FakewareConfigFn,\n): FakewareUserConfig | FakewareConfigFn {\n return config\n}\n","export class ConfigError extends Error {}\n","import { ConfigError } from './errors'\n\nconst ENV_REF = /^\\$([A-Z0-9_]+)$/\n\nexport function interpolate<T>(value: T, env: Record<string, string | undefined>): T {\n if (typeof value === 'string') {\n const match = ENV_REF.exec(value)\n if (!match) return value\n const name = match[1] as string\n const resolved = env[name]\n if (resolved === undefined) {\n throw new ConfigError(`Config references $${name}, but it is not set (check your .env).`)\n }\n return resolved as unknown as T\n }\n if (Array.isArray(value)) {\n return value.map((item) => interpolate(item, env)) as unknown as T\n }\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value)) {\n out[k] = interpolate(v, env)\n }\n return out as T\n }\n return value\n}\n","import { z } from 'zod'\n\nexport const shopwareSchema = z.object({\n url: z.string().min(1, 'shopware.url is required'),\n clientId: z.string().min(1, 'shopware.clientId is required'),\n clientSecret: z.string().min(1, 'shopware.clientSecret is required'),\n})\n\nexport const fakewareConfigSchema = z.object({\n shopware: shopwareSchema.optional(),\n})\n\nexport type FakewareConfig = z.output<typeof fakewareConfigSchema>\n\nexport type FakewareUserConfig = z.input<typeof fakewareConfigSchema>\n","import { access, readFile } from 'node:fs/promises'\nimport { dirname, isAbsolute, join, resolve } from 'node:path'\nimport { loadModule } from '../runtime'\nimport type { ShopwareConnection } from '../shopware'\nimport type { ConfigEnv, FakewareConfigFn } from './define'\nimport { ConfigError } from './errors'\nimport { interpolate } from './interpolate'\nimport { type FakewareConfig, type FakewareUserConfig, fakewareConfigSchema } from './schema'\n\nexport const DEFAULT_CONFIG_FILENAME = 'fakeware.config.ts'\n\nexport interface LoadConfigOptions {\n cwd?: string\n configFile?: string\n mode?: string\n}\n\nexport interface LoadedConfig {\n config: FakewareConfig\n connection: ShopwareConnection\n configPath: string\n projectRoot: string\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path)\n return true\n } catch {\n return false\n }\n}\n\nasync function findConfig(cwd: string): Promise<string> {\n let dir = cwd\n for (;;) {\n const candidate = join(dir, DEFAULT_CONFIG_FILENAME)\n if (await fileExists(candidate)) return candidate\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n throw new ConfigError(\n `No ${DEFAULT_CONFIG_FILENAME} found in ${cwd} or any parent directory. Run \\`fakeware init\\` first.`,\n )\n}\n\nasync function readEnvFile(projectRoot: string): Promise<Record<string, string>> {\n const path = join(projectRoot, '.env')\n if (!(await fileExists(path))) return {}\n const out: Record<string, string> = {}\n const contents = await readFile(path, 'utf8')\n for (const raw of contents.split('\\n')) {\n const line = raw.trim()\n if (!line || line.startsWith('#')) continue\n const eq = line.indexOf('=')\n if (eq === -1) continue\n const key = line.slice(0, eq).trim()\n let val = line.slice(eq + 1).trim()\n if ((val.startsWith('\"') && val.endsWith('\"')) || (val.startsWith(\"'\") && val.endsWith(\"'\"))) {\n val = val.slice(1, -1)\n }\n out[key] = val\n }\n return out\n}\n\nfunction isConfigFn(value: unknown): value is FakewareConfigFn {\n return typeof value === 'function'\n}\n\nexport async function loadConfig(opts: LoadConfigOptions = {}): Promise<LoadedConfig> {\n const cwd = opts.cwd ?? process.cwd()\n const configPath = opts.configFile\n ? isAbsolute(opts.configFile)\n ? opts.configFile\n : resolve(cwd, opts.configFile)\n : await findConfig(cwd)\n const projectRoot = dirname(configPath)\n\n const env: Record<string, string | undefined> = {\n ...process.env,\n ...(await readEnvFile(projectRoot)),\n }\n\n const mod = await loadModule<{ default?: unknown }>(configPath)\n const exported = mod.default\n if (exported === undefined) {\n throw new ConfigError(`${configPath} must \\`export default defineConfig(...)\\`.`)\n }\n\n const configEnv: ConfigEnv = { env, mode: opts.mode ?? 'development' }\n const raw = isConfigFn(exported) ? exported(configEnv) : (exported as FakewareUserConfig)\n\n const interpolated = interpolate(raw, env)\n\n const parsed = fakewareConfigSchema.safeParse(interpolated)\n if (!parsed.success) {\n throw new ConfigError(`Invalid config in ${configPath}: ${parsed.error.message}`)\n }\n\n const { shopware } = parsed.data\n if (!shopware) {\n throw new ConfigError(\n `No \\`shopware\\` connection configured in ${configPath}. up/down need a shop to talk to.`,\n )\n }\n\n return { config: parsed.data, connection: shopware, configPath, projectRoot }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/shopware/client.ts","../../src/shopware/errors.ts","../../src/shopware/locale.ts","../../src/shopware/operations.ts","../../src/shopware/sink.ts"],"sourcesContent":["import { createAdminAPIClient } from '@shopware/api-client'\nimport type { operations } from '@shopware/api-client/admin-api-types'\nimport type { ShopwareConnection } from './types'\n\nexport type ShopwareClient = ReturnType<typeof createAdminAPIClient<operations>>\n\nexport const REQUEST_TIMEOUT_MS = 120_000\n\nexport function createShopwareClient(connection: ShopwareConnection): ShopwareClient {\n return createAdminAPIClient<operations>({\n baseURL: `${connection.url.replace(/\\/$/, '')}/api`,\n credentials: {\n grant_type: 'client_credentials',\n client_id: connection.clientId,\n client_secret: connection.clientSecret,\n },\n fetchOptions: {\n timeout: REQUEST_TIMEOUT_MS,\n },\n })\n}\n","export class ShopwareConnectionError extends Error {}\n","import { z } from 'zod'\nimport { ShopwareConnectionError } from './errors'\nimport type { ShopInfo } from './types'\n\nconst SYSTEM_LANGUAGE_ID = '2fbb5fe2e29a4d70aa5854ce7ce3e20b'\n\nconst languageRowSchema = z.object({\n id: z.string(),\n locale: z.object({ code: z.string().optional() }).nullish(),\n})\n\nexport type LanguageRow = z.infer<typeof languageRowSchema>\n\nexport function parseLanguageRows(rows: unknown): LanguageRow[] {\n const result = z.array(languageRowSchema).safeParse(rows)\n if (!result.success) {\n throw new ShopwareConnectionError(\n 'Shopware returned an unexpected response shape for languages.',\n )\n }\n return result.data\n}\n\nexport function toShopInfo(rows: LanguageRow[]): ShopInfo {\n const seen = new Set<string>()\n const locales: string[] = []\n let systemLocale: string | undefined\n\n for (const row of rows) {\n const code = row.locale?.code\n if (!code) continue\n if (row.id === SYSTEM_LANGUAGE_ID) systemLocale = code\n if (!seen.has(code)) {\n seen.add(code)\n locales.push(code)\n }\n }\n\n if (locales.length === 0) {\n throw new ShopwareConnectionError('Shopware returned no usable locales.')\n }\n\n return { locales, defaultLocale: systemLocale ?? (locales[0] as string) }\n}\n","import { ApiClientError, type ApiError } from '@shopware/api-client'\nimport { createShopwareClient, REQUEST_TIMEOUT_MS } from './client'\nimport { ShopwareConnectionError } from './errors'\nimport { parseLanguageRows, toShopInfo } from './locale'\nimport type { ShopInfo, ShopwareConnection } from './types'\n\nfunction safeJsonParse<T>(input: string): T | null {\n try {\n return JSON.parse(input) as T\n } catch {\n return null\n }\n}\n\nfunction isTimeoutError(error: unknown): boolean {\n let current: unknown = error\n while (current instanceof Error) {\n if (current.name === 'TimeoutError') return true\n current = current.cause\n }\n return false\n}\n\nfunction missingPrivileges(error: ApiClientError<{ errors: ApiError[] }>): string[] {\n for (const e of error.details.errors) {\n if (e.code !== 'FRAMEWORK__MISSING_PRIVILEGE_ERROR' || !e.detail) continue\n const parsed = safeJsonParse<{ missingPrivileges?: string[] }>(e.detail)\n if (parsed?.missingPrivileges?.length) return parsed.missingPrivileges\n }\n return []\n}\n\nfunction validationMessages(error: ApiClientError<{ errors: ApiError[] }>): string[] {\n return error.details.errors\n .map((e) => {\n const field = e.source?.pointer?.replace(/^\\/\\d+\\/\\d+\\//, '')\n const detail = e.detail ?? e.title ?? 'Invalid value.'\n return field ? `${field}: ${detail}` : detail\n })\n .filter((message, index, all) => all.indexOf(message) === index)\n}\n\nexport function toConnectionError(\n connection: ShopwareConnection,\n error: unknown,\n): ShopwareConnectionError {\n if (isTimeoutError(error)) {\n return new ShopwareConnectionError(\n `${connection.url} did not respond within ${REQUEST_TIMEOUT_MS / 1000}s, the shop may be slow or unreachable.`,\n )\n }\n if (error instanceof ApiClientError) {\n switch (error.status) {\n case 400: {\n const messages = validationMessages(error)\n return new ShopwareConnectionError(\n messages.length\n ? `Shopware rejected the data — ${messages.join(' ')}`\n : `Shopware rejected the request (HTTP 400) from ${connection.url}.`,\n )\n }\n case 401:\n return new ShopwareConnectionError(\n 'Authentication failed — check the client ID and client secret of your integration.',\n )\n case 403: {\n const missing = missingPrivileges(error)\n if (missing.length) {\n return new ShopwareConnectionError(\n `The integration is missing the ${missing.join(', ')} ${missing.length === 1 ? 'privilege' : 'privileges'} — grant them to its role in Settings → System → Integrations.`,\n )\n }\n return new ShopwareConnectionError(\n 'The integration is missing permissions — grant its role admin API access in Settings → System → Integrations.',\n )\n }\n case 404:\n return new ShopwareConnectionError(\n `No Shopware admin API found at ${connection.url} — check the shop URL.`,\n )\n default:\n if (error.status >= 500) {\n return new ShopwareConnectionError(\n `${connection.url} is not responding (HTTP ${error.status}) — the shop may be down or in maintenance.`,\n )\n }\n return new ShopwareConnectionError(\n `Shopware returned an unexpected response (HTTP ${error.status}) from ${connection.url}.`,\n )\n }\n }\n return new ShopwareConnectionError(\n `Could not reach ${connection.url} — check the URL and your network connection.`,\n )\n}\n\nexport async function validateConnection(connection: ShopwareConnection): Promise<void> {\n const client = createShopwareClient(connection)\n try {\n await client.invoke('infoShopwareVersion get /_info/version')\n } catch (error) {\n throw toConnectionError(connection, error)\n }\n}\n\nexport async function fetchShopInfo(connection: ShopwareConnection): Promise<ShopInfo> {\n const client = createShopwareClient(connection)\n try {\n const { data } = await client.invoke('searchLanguage post /search/language', {\n body: { associations: { locale: {} }, limit: 500 },\n })\n return toShopInfo(parseLanguageRows(data.data ?? []))\n } catch (error) {\n throw toConnectionError(connection, error)\n }\n}\n","import type { ShopwareSink, SinkRecord } from '../domain'\nimport { createShopwareClient, type ShopwareClient } from './client'\nimport { toConnectionError } from './operations'\nimport type { ShopwareConnection } from './types'\n\nexport interface SyncSinkOptions {\n client?: ShopwareClient\n}\n\nconst SYNC_BATCH_SIZE = 50\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\nexport function createSyncSink(\n connection: ShopwareConnection,\n options: SyncSinkOptions = {},\n): ShopwareSink {\n const client = options.client ?? createShopwareClient(connection)\n\n async function sync(\n entity: string,\n action: 'upsert' | 'delete',\n payload: Record<string, unknown>[],\n ): Promise<void> {\n for (const batch of chunk(payload, SYNC_BATCH_SIZE)) {\n try {\n await client.invoke('sync post /_action/sync', {\n headers: { 'indexing-behavior': 'use-queue-indexing' },\n body: [{ entity, action, payload: batch as never }],\n })\n } catch (error) {\n throw toConnectionError(connection, error)\n }\n }\n }\n\n return {\n async upsert(entity: string, records: SinkRecord[]): Promise<void> {\n if (records.length > 0) await sync(entity, 'upsert', records)\n },\n async delete(entity: string, ids: string[]): Promise<void> {\n if (ids.length > 0)\n await sync(\n entity,\n 'delete',\n ids.map((id) => ({ id })),\n )\n },\n }\n}\n"],"mappings":"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"}