@endora-commerce/mod-import-export 0.100.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/admin/index.d.ts +30 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +36 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/ImportExportPage.d.ts +3 -0
  8. package/dist/admin/pages/ImportExportPage.d.ts.map +1 -0
  9. package/dist/admin/pages/ImportExportPage.js +71 -0
  10. package/dist/admin/pages/ImportExportPage.js.map +1 -0
  11. package/dist/backend/index.d.ts +43 -0
  12. package/dist/backend/index.d.ts.map +1 -0
  13. package/dist/backend/index.js +54 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/backend/routes.d.ts +9 -0
  16. package/dist/backend/routes.d.ts.map +1 -0
  17. package/dist/backend/routes.js +44 -0
  18. package/dist/backend/routes.js.map +1 -0
  19. package/dist/backend/services/adapter.d.ts +59 -0
  20. package/dist/backend/services/adapter.d.ts.map +1 -0
  21. package/dist/backend/services/adapter.js +2 -0
  22. package/dist/backend/services/adapter.js.map +1 -0
  23. package/dist/backend/services/adapters/categories.adapter.d.ts +19 -0
  24. package/dist/backend/services/adapters/categories.adapter.d.ts.map +1 -0
  25. package/dist/backend/services/adapters/categories.adapter.js +93 -0
  26. package/dist/backend/services/adapters/categories.adapter.js.map +1 -0
  27. package/dist/backend/services/adapters/customers.adapter.d.ts +16 -0
  28. package/dist/backend/services/adapters/customers.adapter.d.ts.map +1 -0
  29. package/dist/backend/services/adapters/customers.adapter.js +48 -0
  30. package/dist/backend/services/adapters/customers.adapter.js.map +1 -0
  31. package/dist/backend/services/adapters/orders.adapter.d.ts +13 -0
  32. package/dist/backend/services/adapters/orders.adapter.d.ts.map +1 -0
  33. package/dist/backend/services/adapters/orders.adapter.js +48 -0
  34. package/dist/backend/services/adapters/orders.adapter.js.map +1 -0
  35. package/dist/backend/services/adapters/products.adapter.d.ts +16 -0
  36. package/dist/backend/services/adapters/products.adapter.d.ts.map +1 -0
  37. package/dist/backend/services/adapters/products.adapter.js +88 -0
  38. package/dist/backend/services/adapters/products.adapter.js.map +1 -0
  39. package/dist/backend/services/adapters/stock.adapter.d.ts +15 -0
  40. package/dist/backend/services/adapters/stock.adapter.d.ts.map +1 -0
  41. package/dist/backend/services/adapters/stock.adapter.js +62 -0
  42. package/dist/backend/services/adapters/stock.adapter.js.map +1 -0
  43. package/dist/backend/services/csv-codec.d.ts +17 -0
  44. package/dist/backend/services/csv-codec.d.ts.map +1 -0
  45. package/dist/backend/services/csv-codec.js +95 -0
  46. package/dist/backend/services/csv-codec.js.map +1 -0
  47. package/dist/backend/services/import-export-service.d.ts +52 -0
  48. package/dist/backend/services/import-export-service.d.ts.map +1 -0
  49. package/dist/backend/services/import-export-service.js +100 -0
  50. package/dist/backend/services/import-export-service.js.map +1 -0
  51. package/dist/manifest.d.ts +169 -0
  52. package/dist/manifest.d.ts.map +1 -0
  53. package/dist/manifest.js +138 -0
  54. package/dist/manifest.js.map +1 -0
  55. package/docs/import_export.md +79 -0
  56. package/i18n/en.json +27 -0
  57. package/i18n/pl.json +27 -0
  58. package/package.json +82 -0
  59. package/tailwind.css +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Endora sp. z o.o. and the Endora Commerce contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ <!-- Generated by `pnpm --filter backend run manifests:generate`. Delete this line to take the file over; a README without it is never regenerated. -->
2
+
3
+ # @endora-commerce/mod-import-export
4
+
5
+ Bulk product import and export wizard.
6
+
7
+ ## What this is
8
+
9
+ An **Endora Commerce module package**. Its module id is `import_export` — the identity of record in the platform's registries, in its migrations and in its setting, permission and translation codes.
10
+
11
+ A module is not imported by application code. The platform discovers the extension packages an instance has installed, reads the manifest on the root subpath and composes the module from it, so installing this package and starting the instance is the whole integration.
12
+
13
+ ## Entry points
14
+
15
+ | Import | Contents |
16
+ | --- | --- |
17
+ | `@endora-commerce/mod-import-export` | the module manifest — its id, version, dependencies, settings and activation |
18
+ | `@endora-commerce/mod-import-export/admin` | the Admin UI contribution: the screens, navigation and palette actions it adds |
19
+ | `@endora-commerce/mod-import-export/backend` | the composition root the platform calls, with the entities, services, routes and workers it registers |
20
+ | `@endora-commerce/mod-import-export/tailwind.css` | the Tailwind source declaration for the admin code above |
21
+
22
+ ## Depends on
23
+
24
+ Everything below is a **peer** dependency, so the application resolves exactly one copy of each — two copies of a host or of React are a runtime failure rather than a type error. An entry marked *optional* is needed only by the layers that use it.
25
+
26
+ **Endora packages**
27
+
28
+ - `@endora-commerce/admin-kit` — *optional*
29
+ - `@endora-commerce/contracts`
30
+ - `@endora-commerce/platform`
31
+
32
+ **Third-party**
33
+
34
+ - `fastify` ^5
35
+ - `lucide-react` ^1 — *optional*
36
+ - `react` ^19 — *optional*
37
+
38
+ ## What the tarball carries
39
+
40
+ - `dist/` — the compiled JavaScript and its type declarations
41
+ - `i18n/` — the translation bundles this package ships (`en`, `pl`), loaded by the platform
42
+ - `docs/` — the package’s own pages in the Endora Commerce documentation site
43
+ - `tailwind.css` — the Tailwind source declaration
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pnpm add @endora-commerce/mod-import-export
49
+ ```
50
+
51
+ ## Licence
52
+
53
+ MIT — the text is in `LICENSE`, beside this file.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `import_export`'s admin surface — one route and one sidebar entry, declared
3
+ * by the module that owns them (feature 091, Phase 2;
4
+ * `specs/091-module-owned-admin-surfaces/contracts/admin-contribution.md`).
5
+ *
6
+ * Until this file existed, adding an admin screen meant editing
7
+ * `admin/src/App.tsx` and `admin/src/components/AppShell.tsx` — two files no
8
+ * module owns, which 11 of the 12 most recently added modules edited, so two
9
+ * authors working in the same week collided even when the platform team changed
10
+ * nothing (`spec.md` §1.1). `admin/src/modules.generated.ts` now imports this
11
+ * object by the bare specifier this package's own `exports` map declares, and
12
+ * the admin renders `[...host, ...registry]` through the one visibility
13
+ * predicate it already had.
14
+ *
15
+ * **This entry exports data and nothing else** (R2). It may not export a
16
+ * component, a hook or a service: `check:module-boundary`'s D-171 rule
17
+ * designates a subpath as *contract surface* when it emits no runtime binding,
18
+ * and `./admin` deliberately does not qualify — it exports an object — so a
19
+ * consumer reaching into another module's `./admin` stays a counted boundary
20
+ * reach, which is the correct answer.
21
+ *
22
+ * **The component is a dynamic-import factory, and it is the only
23
+ * function-valued field** (R6). A static import would defeat FR-013 — an
24
+ * operator downloads the page code of a module their role cannot reach — and
25
+ * would make the registry evaluate React to be enumerated. Vite therefore emits
26
+ * one chunk for this screen by construction rather than by a build rule.
27
+ */
28
+ import type { AdminContributions } from '@endora-commerce/admin-kit/contributions';
29
+ export declare const contributions: AdminContributions;
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0CAA0C,CAAC;AAKnF,eAAO,MAAM,aAAa,EAAE,kBAgC3B,CAAC"}
@@ -0,0 +1,36 @@
1
+ /** The centre's one route. `catalog:write` is what its API routes enforce. */
2
+ const ROUTE_PATH = '/import-export';
3
+ export const contributions = {
4
+ routes: [
5
+ {
6
+ path: ROUTE_PATH,
7
+ component: () => import('./pages/ImportExportPage.js'),
8
+ // The code `import_export`'s own admin routes are gated by
9
+ // (`requireAdmin('catalog:write')`), read from the route rather than
10
+ // copied from a neighbour — the rule `check:action-route-permissions`
11
+ // already holds this module's two palette actions to, and which both of
12
+ // them declare identically.
13
+ requiredPermission: 'catalog:write',
14
+ index: true,
15
+ },
16
+ ],
17
+ nav: [
18
+ {
19
+ to: ROUTE_PATH,
20
+ // Module-relative (R8), resolved in this module's own namespace out of
21
+ // `packages/modules/import_export/i18n/`. It was `appShell.nav.importExport`
22
+ // in the shared `_i18n` bundle, one of the four shared files a module
23
+ // author had to edit.
24
+ labelKey: 'nav.importExport.label',
25
+ icon: 'Upload',
26
+ // Bulk operations span many domains, so the entry belongs under System
27
+ // rather than to any one of them — the section it has always rendered in.
28
+ section: 'system',
29
+ // The weight the hand-written NAV gave it by position: between
30
+ // `credentials` and `settings` in the System section.
31
+ weight: 700,
32
+ requiredPermission: 'catalog:write',
33
+ },
34
+ ],
35
+ };
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AA6BA,8EAA8E;AAC9E,MAAM,UAAU,GAAG,gBAAgB,CAAC;AAEpC,MAAM,CAAC,MAAM,aAAa,GAAuB;IAC/C,MAAM,EAAE;QACN;YACE,IAAI,EAAE,UAAU;YAChB,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,6BAA6B,CAAC;YACtD,2DAA2D;YAC3D,qEAAqE;YACrE,sEAAsE;YACtE,wEAAwE;YACxE,4BAA4B;YAC5B,kBAAkB,EAAE,eAAe;YACnC,KAAK,EAAE,IAAI;SACZ;KACF;IACD,GAAG,EAAE;QACH;YACE,EAAE,EAAE,UAAU;YACd,uEAAuE;YACvE,6EAA6E;YAC7E,sEAAsE;YACtE,sBAAsB;YACtB,QAAQ,EAAE,wBAAwB;YAClC,IAAI,EAAE,QAAQ;YACd,uEAAuE;YACvE,0EAA0E;YAC1E,OAAO,EAAE,QAAQ;YACjB,+DAA+D;YAC/D,sDAAsD;YACtD,MAAM,EAAE,GAAG;YACX,kBAAkB,EAAE,eAAe;SACpC;KACF;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type ReactNode } from 'react';
2
+ export default function ImportExportPage(): ReactNode;
3
+ //# sourceMappingURL=ImportExportPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImportExportPage.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/ImportExportPage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAsD3F,MAAM,CAAC,OAAO,UAAU,gBAAgB,IAAI,SAAS,CAmDpD"}
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { Download, Upload } from 'lucide-react';
4
+ import { ApiError, apiBaseUrl, apiClient } from '@endora-commerce/admin-kit/lib';
5
+ import { Alert, AlertDescription, Button, Card, CardContent, CardHeader, CardTitle, PageHeader, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@endora-commerce/admin-kit/ui';
6
+ import { useTranslation } from '@endora-commerce/admin-kit/i18n';
7
+ export default function ImportExportPage() {
8
+ const t = useTranslation('import_export');
9
+ const [entities, setEntities] = useState([]);
10
+ const [loading, setLoading] = useState(true);
11
+ const [loadError, setLoadError] = useState(null);
12
+ const load = useCallback(async () => {
13
+ setLoading(true);
14
+ setLoadError(null);
15
+ try {
16
+ const res = await apiClient.get('/api/v1/admin/import-export/entities');
17
+ setEntities(res.data.entities);
18
+ }
19
+ catch (err) {
20
+ setLoadError(err instanceof ApiError ? err.envelope.error.message : t('error.load'));
21
+ }
22
+ finally {
23
+ setLoading(false);
24
+ }
25
+ }, [t]);
26
+ useEffect(() => {
27
+ void load();
28
+ }, [load]);
29
+ return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { title: t('page.title'), description: t('page.description') }), loadError ? (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: loadError }) })) : null, loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('loading') })) : null, !loading && !loadError && entities.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('empty') })) : null, _jsx("div", { className: "space-y-4", children: entities.map((entity) => (_jsx(EntityCard, { entity: entity }, entity.name))) })] }));
30
+ }
31
+ function EntityCard({ entity }) {
32
+ const t = useTranslation('import_export');
33
+ const [importing, setImporting] = useState(false);
34
+ const [importMessage, setImportMessage] = useState(null);
35
+ const [importErrors, setImportErrors] = useState([]);
36
+ const [importError, setImportError] = useState(null);
37
+ const onFileChange = useCallback(async (event) => {
38
+ const file = event.target.files?.[0];
39
+ event.target.value = '';
40
+ if (!file)
41
+ return;
42
+ setImporting(true);
43
+ setImportMessage(null);
44
+ setImportErrors([]);
45
+ setImportError(null);
46
+ try {
47
+ const csv = await file.text();
48
+ const res = await apiClient.post(`/api/v1/admin/import/${entity.name}`, csv, {
49
+ headers: { 'Content-Type': 'text/csv' },
50
+ });
51
+ if (res.data.errors.length === 0) {
52
+ setImportMessage(t('imported', { count: res.data.imported }));
53
+ }
54
+ else {
55
+ setImportMessage(t('rolledBack', { count: res.data.errors.length }));
56
+ setImportErrors(res.data.errors);
57
+ }
58
+ }
59
+ catch (err) {
60
+ setImportError(err instanceof ApiError ? err.envelope.error.message : t('error.import'));
61
+ }
62
+ finally {
63
+ setImporting(false);
64
+ }
65
+ }, [entity.name, t]);
66
+ const exportHref = `${apiBaseUrl}/api/v1/admin/export/${entity.name}.csv`;
67
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t(`entity.${entity.name}`) }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs("a", { href: exportHref, download: true, children: [_jsx(Download, {}), t('exportCsv')] }) }), entity.importHeader ? (_jsx(Button, { asChild: true, size: "sm", disabled: importing, className: importing ? 'cursor-wait opacity-70' : 'cursor-pointer', children: _jsxs("label", { className: "relative", children: [_jsx(Upload, {}), importing ? t('uploading') : t('importCsv'), _jsx("input", { type: "file", accept: ".csv,text/csv", onChange: (e) => {
68
+ void onFileChange(e);
69
+ }, disabled: importing, className: "absolute inset-0 cursor-[inherit] opacity-0" })] }) })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: t('importNotSupported') }))] }), entity.importHeader ? (_jsxs("p", { className: "text-xs text-muted-foreground", children: [t('requiredHeader'), ' ', _jsx("code", { className: "rounded bg-muted px-1 font-mono", children: entity.importHeader.join(', ') })] })) : null, importError ? (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: importError }) })) : null, importMessage ? (_jsx(Alert, { variant: importErrors.length > 0 ? 'warning' : 'success', children: _jsx(AlertDescription, { children: importMessage }) })) : null, importErrors.length > 0 ? (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t('column.rowNumber') }), _jsx(TableHead, { children: t('column.reason') })] }) }), _jsx(TableBody, { children: importErrors.map((err) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: err.rowNumber }), _jsx(TableCell, { children: err.reason })] }, `${err.rowNumber}-${err.reason}`))) })] })) : null] })] }));
70
+ }
71
+ //# sourceMappingURL=ImportExportPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImportExportPage.js","sourceRoot":"","sources":["../../../src/admin/pages/ImportExportPage.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
2
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
3
+ import { ImportExportService } from './services/import-export-service.js';
4
+ /**
5
+ * `import_export` — the shortest conversion in the wave (feature 072, wave 2,
6
+ * T122).
7
+ *
8
+ * Nothing about it is unusual, which is worth one sentence rather than none:
9
+ * the module took two arguments, both roots passed both, neither consumed its
10
+ * handle, and it owns no entity and no migration. Deleting the directory now
11
+ * leaves nothing behind at all, so it does not appear in the residue ledger —
12
+ * the fourth module in the transition to reach that state, after
13
+ * `health_checks`, `audit_logs` and `google_tag_manager`.
14
+ *
15
+ * The one thing to preserve is the `text/csv` body parser. It is registered
16
+ * inside `ctx.routes`, which hands this module an **encapsulated** Fastify
17
+ * context, so the parser applies to this module's routes and nowhere else.
18
+ * That is what we want — a CSV parser is not something the rest of the platform
19
+ * should acquire because one admin endpoint accepts a CSV upload — and it is
20
+ * also stricter than the old arrangement, where the plugin was registered onto
21
+ * whatever context a root handed it.
22
+ */
23
+ export interface ImportExportCradle {
24
+ readonly requireAdmin: RequireAdminFactory;
25
+ readonly importExportService: ImportExportService;
26
+ }
27
+ export declare function registerModule(ctx: ModuleContext): void;
28
+ /**
29
+ * This module owns **no persisted entity**, and says so with an empty array
30
+ * rather than by omission (D-168).
31
+ *
32
+ * The two are not the same thing to the platform. When the package is
33
+ * *installed*, `src/packages/package-runtime.ts` reads `exported['entities']`
34
+ * and answers a missing export with `[]` — so "this module has no table" and
35
+ * "somebody forgot the array" arrive at the host as the same silence, and the
36
+ * only symptom of the second is a query against a table nobody created. The
37
+ * declaration is what makes the first case a statement.
38
+ *
39
+ * A first entity added here goes in this array in the same merge request, and
40
+ * `test/unit/packages/module-package-entity-surface.test.ts` is what says so.
41
+ */
42
+ export declare const entities: readonly never[];
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAG1E;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;CACnD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAyCvD;AAGD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,EAAE,SAAS,KAAK,EAAO,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { ImportExportService } from './services/import-export-service.js';
3
+ import { registerImportExportRoutes } from './routes.js';
4
+ export function registerModule(ctx) {
5
+ ctx.di.register({
6
+ /**
7
+ * D-74 — the seven edges, all of them ports now.
8
+ *
9
+ * Each is a `lazyPort`, so the owner's presence gate is asked at the call
10
+ * rather than at construction; the service also probes presence *before* it
11
+ * offers an entity, which is what turns a switched-off owner into an entity
12
+ * this deployment does not have rather than a 503 an operator has to read.
13
+ */
14
+ importExportService: ctx
15
+ .asFunction(() => new ImportExportService({
16
+ catalogProducts: lazyPort(ctx, 'catalogProductReadPort'),
17
+ catalogCategories: lazyPort(ctx, 'catalogCategoryReadPort'),
18
+ catalogBulkImport: lazyPort(ctx, 'catalogBulkImportPort'),
19
+ inventoryStock: lazyPort(ctx, 'inventoryStockReadPort'),
20
+ inventoryStockImport: lazyPort(ctx, 'inventoryStockImportPort'),
21
+ orders: lazyPort(ctx, 'orderReadPort'),
22
+ customerAccounts: lazyPort(ctx, 'customerAccountReadPort'),
23
+ organizations: lazyPort(ctx, 'organizationDetailsPort'),
24
+ }))
25
+ .singleton(),
26
+ });
27
+ ctx.routes(async (app) => {
28
+ const { importExportService, requireAdmin } = ctx.cradle();
29
+ // A raw `text/csv` body, so `POST /admin/import/:entity` needs no multipart
30
+ // machinery. Scoped to this module's context by `ctx.routes`.
31
+ if (!app.hasContentTypeParser('text/csv')) {
32
+ app.addContentTypeParser('text/csv', { parseAs: 'string' }, (_req, body, done) => {
33
+ done(null, body);
34
+ });
35
+ }
36
+ await registerImportExportRoutes(app, { service: importExportService, requireAdmin });
37
+ });
38
+ }
39
+ /**
40
+ * This module owns **no persisted entity**, and says so with an empty array
41
+ * rather than by omission (D-168).
42
+ *
43
+ * The two are not the same thing to the platform. When the package is
44
+ * *installed*, `src/packages/package-runtime.ts` reads `exported['entities']`
45
+ * and answers a missing export with `[]` — so "this module has no table" and
46
+ * "somebody forgot the array" arrive at the host as the same silence, and the
47
+ * only symptom of the second is a query against a table nobody created. The
48
+ * declaration is what makes the first case a statement.
49
+ *
50
+ * A first entity added here goes in this array in the same merge request, and
51
+ * `test/unit/packages/module-package-entity-surface.test.ts` is what says so.
52
+ */
53
+ export const entities = [];
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAE5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AA2BzD,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;QACd;;;;;;;WAOG;QACH,mBAAmB,EAAE,GAAG;aACrB,UAAU,CACT,GAAG,EAAE,CACH,IAAI,mBAAmB,CAAC;YACtB,eAAe,EAAE,QAAQ,CAAyB,GAAG,EAAE,wBAAwB,CAAC;YAChF,iBAAiB,EAAE,QAAQ,CAA0B,GAAG,EAAE,yBAAyB,CAAC;YACpF,iBAAiB,EAAE,QAAQ,CAAwB,GAAG,EAAE,uBAAuB,CAAC;YAChF,cAAc,EAAE,QAAQ,CAAyB,GAAG,EAAE,wBAAwB,CAAC;YAC/E,oBAAoB,EAAE,QAAQ,CAC5B,GAAG,EACH,0BAA0B,CAC3B;YACD,MAAM,EAAE,QAAQ,CAAgB,GAAG,EAAE,eAAe,CAAC;YACrD,gBAAgB,EAAE,QAAQ,CAA0B,GAAG,EAAE,yBAAyB,CAAC;YACnF,aAAa,EAAE,QAAQ,CAA0B,GAAG,EAAE,yBAAyB,CAAC;SACjF,CAAC,CACL;aACA,SAAS,EAAE;KACf,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,EAAE,mBAAmB,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,MAAM,EAAsB,CAAC;QAC/E,4EAA4E;QAC5E,8DAA8D;QAC9D,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,UAAU,CAAC,EAAE,CAAC;YAC1C,GAAG,CAAC,oBAAoB,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;gBAC/E,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACnB,CAAC,CAAC,CAAC;QACL,CAAC;QACD,MAAM,0BAA0B,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,CAAC,CAAC;IACxF,CAAC,CAAC,CAAC;AACL,CAAC;AAGD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAqB,EAAE,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { ImportExportService } from './services/import-export-service.js';
3
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
4
+ export interface ImportExportRoutesDeps {
5
+ service: ImportExportService;
6
+ requireAdmin: RequireAdminFactory;
7
+ }
8
+ export declare function registerImportExportRoutes(app: FastifyInstance, deps: ImportExportRoutesDeps): Promise<void>;
9
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAID,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CA4Df"}
@@ -0,0 +1,44 @@
1
+ const FILENAME_RE = /^[a-z][a-z0-9_]*$/;
2
+ export async function registerImportExportRoutes(app, deps) {
3
+ const { service, requireAdmin } = deps;
4
+ /**
5
+ * The entities this deployment can offer, D-74.
6
+ *
7
+ * The admin renders from this instead of the literal five-slug array it used
8
+ * to carry, so switching `orders` off removes the Orders export from the
9
+ * screen rather than leaving a button that 404s. `SUPPORTED_EXPORT_ENTITIES`
10
+ * and `SUPPORTED_IMPORT_ENTITIES` were the static half of this answer and had
11
+ * no reader at all; they are gone rather than left as a second source of
12
+ * truth beside this one.
13
+ */
14
+ app.get('/api/v1/admin/import-export/entities', { preHandler: requireAdmin('catalog:write') }, async () => ({ data: { entities: service.listEntities() } }));
15
+ app.get('/api/v1/admin/export/:entity.csv', { preHandler: requireAdmin('catalog:write'), config: { streamingResponse: true } }, async (request, reply) => {
16
+ const { entity } = request.params;
17
+ if (!FILENAME_RE.test(entity)) {
18
+ reply.status(400);
19
+ return { error: { code: 'VALIDATION_FAILED', message: 'invalid entity slug' } };
20
+ }
21
+ const body = await service.exportToCsv(entity);
22
+ reply
23
+ .header('Content-Type', 'text/csv; charset=utf-8')
24
+ .header('Content-Disposition', `attachment; filename="${entity}-${new Date().toISOString().slice(0, 10)}.csv"`);
25
+ return body;
26
+ });
27
+ app.post('/api/v1/admin/import/:entity', {
28
+ preHandler: requireAdmin('catalog:write'),
29
+ // Receive the raw CSV body — operators can `curl --data-binary @file.csv`
30
+ // and the admin SPA can read the file as text and POST it directly.
31
+ }, async (request, reply) => {
32
+ const { entity } = request.params;
33
+ if (!FILENAME_RE.test(entity)) {
34
+ reply.status(400);
35
+ return { error: { code: 'VALIDATION_FAILED', message: 'invalid entity slug' } };
36
+ }
37
+ const body = typeof request.body === 'string'
38
+ ? request.body
39
+ : request.body?.toString('utf8') ?? '';
40
+ const report = await service.importFromCsv(entity, body);
41
+ return { data: report };
42
+ });
43
+ }
44
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAExC,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,GAAoB,EACpB,IAA4B;IAE5B,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAEvC;;;;;;;;;OASG;IACH,GAAG,CAAC,GAAG,CACL,sCAAsC,EACtC,EAAE,UAAU,EAAE,YAAY,CAAC,eAAe,CAAC,EAAE,EAC7C,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,YAAY,EAAE,EAAE,EAAE,CAAC,CAC7D,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,kCAAkC,EAClC,EAAE,UAAU,EAAE,YAAY,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,EAClF,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QAClC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9B,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAClB,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,qBAAqB,EAAE,EAAE,CAAC;QAClF,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAC/C,KAAK;aACF,MAAM,CAAC,cAAc,EAAE,yBAAyB,CAAC;aACjD,MAAM,CACL,qBAAqB,EACrB,yBAAyB,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAChF,CAAC;QACJ,OAAO,IAAI,CAAC;IACd,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,IAAI,CACN,8BAA8B,EAC9B;QACE,UAAU,EAAE,YAAY,CAAC,eAAe,CAAC;QACzC,0EAA0E;QAC1E,oEAAoE;KACrE,EACD,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QAClC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9B,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAClB,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,qBAAqB,EAAE,EAAE,CAAC;QAClF,CAAC;QACD,MAAM,IAAI,GACR,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ;YAC9B,CAAC,CAAC,OAAO,CAAC,IAAI;YACd,CAAC,CAAE,OAAO,CAAC,IAAsC,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC9E,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACzD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,59 @@
1
+ import type { BulkImportReport, CatalogBulkImportPort, CatalogCategoryReadPort, CatalogProductReadPort, CustomerAccountReadPort, InventoryStockImportPort, InventoryStockReadPort, OrderReadPort, OrganizationDetailsPort } from '@endora-commerce/contracts';
2
+ /**
3
+ * What an adapter is allowed to reach (feature 075, D-74).
4
+ *
5
+ * Before the cut every adapter took an `EntityManager` and queried four other
6
+ * modules' entity classes with it — and, in the three importing cases, wrote
7
+ * them inside a transaction this module opened. It reaches the same rows
8
+ * through the owners' published ports now, and the three imports hand their rows
9
+ * to the owner and render what comes back.
10
+ *
11
+ * Each entry is a lazily resolved port, so a call arrives at the owner's
12
+ * presence gate rather than at a handle captured while it was still switched on.
13
+ */
14
+ export interface ImportExportPorts {
15
+ readonly catalogProducts: CatalogProductReadPort;
16
+ readonly catalogCategories: CatalogCategoryReadPort;
17
+ readonly catalogBulkImport: CatalogBulkImportPort;
18
+ readonly inventoryStock: InventoryStockReadPort;
19
+ readonly inventoryStockImport: InventoryStockImportPort;
20
+ readonly orders: OrderReadPort;
21
+ readonly customerAccounts: CustomerAccountReadPort;
22
+ readonly organizations: OrganizationDetailsPort;
23
+ }
24
+ export interface ImportExportAdapter {
25
+ /** Stable URL slug, e.g. `products`, `categories`. */
26
+ name: string;
27
+ /**
28
+ * The modules whose rows this entity is. An entity is offered only while
29
+ * **every** one of them is effectively present (Principle XVII rule 5): an
30
+ * operator who switches `inventory` off must not be shown a Stock import, and
31
+ * a POST naming it gets the same 404 an unknown slug gets.
32
+ *
33
+ * Two owners is not a special case — the customers export names an account's
34
+ * organisation, so it needs both modules to answer.
35
+ */
36
+ owners: readonly string[];
37
+ /** Header columns emitted on export. */
38
+ exportHeader: readonly string[];
39
+ /** The entity table as CSV-ready rows, read through the owners' ports. */
40
+ exportRows(): Promise<string[][]>;
41
+ /** Header columns the import expects. Undefined when import is unsupported. */
42
+ importHeader?: readonly string[];
43
+ /**
44
+ * Map the parsed CSV records onto the owner's import port and return its
45
+ * report.
46
+ *
47
+ * The index in the report is 0-based over `records`; turning it into the row
48
+ * number a spreadsheet displays is the service's job, in one place, because
49
+ * the owner has no idea a row came from a file at all.
50
+ *
51
+ * Column-level validation ("invalid is_active: xyz") is raised here, where the
52
+ * CSV vocabulary lives; row-level validation against the stored data
53
+ * ("unknown parent_slug: x") is raised by the owner, where the answer lives.
54
+ * A local failure short-circuits the run before any port is called, which is what
55
+ * keeps all-or-nothing true across the two halves.
56
+ */
57
+ importRows?(records: ReadonlyArray<Record<string, string>>): Promise<BulkImportReport>;
58
+ }
59
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/backend/services/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,EACtB,aAAa,EACb,uBAAuB,EACxB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,eAAe,EAAE,sBAAsB,CAAC;IACjD,QAAQ,CAAC,iBAAiB,EAAE,uBAAuB,CAAC;IACpD,QAAQ,CAAC,iBAAiB,EAAE,qBAAqB,CAAC;IAClD,QAAQ,CAAC,cAAc,EAAE,sBAAsB,CAAC;IAChD,QAAQ,CAAC,oBAAoB,EAAE,wBAAwB,CAAC;IACxD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,uBAAuB,CAAC;IACnD,QAAQ,CAAC,aAAa,EAAE,uBAAuB,CAAC;CACjD;AAED,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;OAQG;IACH,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,wCAAwC;IACxC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,0EAA0E;IAC1E,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAClC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACxF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../../src/backend/services/adapter.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import type { ImportExportAdapter, ImportExportPorts } from '../adapter.js';
2
+ /**
3
+ * Categories import/export. The CSV uses `parent_slug` to keep a parent
4
+ * reference human-readable; an empty value means "root". Categories are
5
+ * processed in the order they appear, so spreadsheets must list parents
6
+ * before their children.
7
+ *
8
+ * Feature 068 — `is_active` rides along in both directions so a round-trip
9
+ * through a spreadsheet does not silently re-publish a hidden category. An
10
+ * absent or empty cell leaves the stored value alone (and defaults to active
11
+ * on create), which keeps older exports importable.
12
+ *
13
+ * Feature 075 / D-74 — the rows are `catalog`'s, so `catalog` applies them.
14
+ * What is left here is the CSV vocabulary: which columns exist, how a cell
15
+ * becomes a field, and which cell is malformed. "Unknown parent_slug" is not in
16
+ * that list — it is an answer only the catalogue has.
17
+ */
18
+ export declare function categoriesAdapter(ports: ImportExportPorts): ImportExportAdapter;
19
+ //# sourceMappingURL=categories.adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"categories.adapter.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/adapters/categories.adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE5E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,mBAAmB,CAiF/E"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Categories import/export. The CSV uses `parent_slug` to keep a parent
3
+ * reference human-readable; an empty value means "root". Categories are
4
+ * processed in the order they appear, so spreadsheets must list parents
5
+ * before their children.
6
+ *
7
+ * Feature 068 — `is_active` rides along in both directions so a round-trip
8
+ * through a spreadsheet does not silently re-publish a hidden category. An
9
+ * absent or empty cell leaves the stored value alone (and defaults to active
10
+ * on create), which keeps older exports importable.
11
+ *
12
+ * Feature 075 / D-74 — the rows are `catalog`'s, so `catalog` applies them.
13
+ * What is left here is the CSV vocabulary: which columns exist, how a cell
14
+ * becomes a field, and which cell is malformed. "Unknown parent_slug" is not in
15
+ * that list — it is an answer only the catalogue has.
16
+ */
17
+ export function categoriesAdapter(ports) {
18
+ return {
19
+ name: 'categories',
20
+ owners: ['catalog'],
21
+ exportHeader: ['id', 'slug', 'parent_slug', 'sort_order', 'name_en', 'is_active'],
22
+ async exportRows() {
23
+ const rows = await ports.catalogCategories.listAll();
24
+ const slugById = new Map(rows.map((c) => [c.id, c.slug]));
25
+ // By slug, as the export has always been. The port orders by sort order
26
+ // then slug, because that is what its other eight callers want.
27
+ return [...rows]
28
+ .sort((a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0))
29
+ .map((c) => [
30
+ c.id,
31
+ c.slug,
32
+ c.parentCategoryId ? (slugById.get(c.parentCategoryId) ?? '') : '',
33
+ String(c.sortOrder),
34
+ c.name['en-US'] ?? '',
35
+ c.isActive ? 'true' : 'false',
36
+ ]);
37
+ },
38
+ // `is_active` is deliberately NOT listed here: `importHeader` is the set of
39
+ // columns the CSV must carry, and requiring it would reject every
40
+ // spreadsheet produced before this feature.
41
+ importHeader: ['slug', 'parent_slug', 'sort_order', 'name_en'],
42
+ async importRows(records) {
43
+ const errors = [];
44
+ const rows = [];
45
+ for (const [index, record] of records.entries()) {
46
+ const slug = record['slug']?.trim();
47
+ if (!slug) {
48
+ errors.push({ index, reason: 'slug is required' });
49
+ continue;
50
+ }
51
+ const nameEn = record['name_en']?.trim();
52
+ if (!nameEn) {
53
+ errors.push({ index, reason: 'name_en is required' });
54
+ continue;
55
+ }
56
+ const sortOrderRaw = record['sort_order']?.trim();
57
+ const sortOrder = sortOrderRaw ? Number.parseInt(sortOrderRaw, 10) : 0;
58
+ if (Number.isNaN(sortOrder)) {
59
+ errors.push({ index, reason: `invalid sort_order: ${sortOrderRaw}` });
60
+ continue;
61
+ }
62
+ const isActiveRaw = record['is_active']?.trim().toLowerCase();
63
+ let isActive;
64
+ if (isActiveRaw !== undefined && isActiveRaw !== '') {
65
+ if (isActiveRaw === 'true' || isActiveRaw === '1')
66
+ isActive = true;
67
+ else if (isActiveRaw === 'false' || isActiveRaw === '0')
68
+ isActive = false;
69
+ else {
70
+ errors.push({ index, reason: `invalid is_active: ${record['is_active']}` });
71
+ continue;
72
+ }
73
+ }
74
+ rows.push({
75
+ slug,
76
+ // Both columns are required headers, so every row states them: an
77
+ // empty `parent_slug` means root rather than "leave the parent alone".
78
+ parentSlug: record['parent_slug']?.trim() || null,
79
+ sortOrder,
80
+ name: { 'en-US': nameEn },
81
+ ...(isActive !== undefined ? { isActive } : {}),
82
+ });
83
+ }
84
+ // A malformed cell stops the run before the owner is asked to do anything:
85
+ // the promise is that either every row lands or none does, and half of the
86
+ // validation living on each side must not weaken it.
87
+ if (errors.length > 0)
88
+ return { imported: 0, errors };
89
+ return ports.catalogBulkImport.importCategories(rows);
90
+ },
91
+ };
92
+ }
93
+ //# sourceMappingURL=categories.adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"categories.adapter.js","sourceRoot":"","sources":["../../../../src/backend/services/adapters/categories.adapter.ts"],"names":[],"mappings":"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"}