@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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/admin/index.d.ts +30 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +36 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/ImportExportPage.d.ts +3 -0
- package/dist/admin/pages/ImportExportPage.d.ts.map +1 -0
- package/dist/admin/pages/ImportExportPage.js +71 -0
- package/dist/admin/pages/ImportExportPage.js.map +1 -0
- package/dist/backend/index.d.ts +43 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +54 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +9 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +44 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/adapter.d.ts +59 -0
- package/dist/backend/services/adapter.d.ts.map +1 -0
- package/dist/backend/services/adapter.js +2 -0
- package/dist/backend/services/adapter.js.map +1 -0
- package/dist/backend/services/adapters/categories.adapter.d.ts +19 -0
- package/dist/backend/services/adapters/categories.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/categories.adapter.js +93 -0
- package/dist/backend/services/adapters/categories.adapter.js.map +1 -0
- package/dist/backend/services/adapters/customers.adapter.d.ts +16 -0
- package/dist/backend/services/adapters/customers.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/customers.adapter.js +48 -0
- package/dist/backend/services/adapters/customers.adapter.js.map +1 -0
- package/dist/backend/services/adapters/orders.adapter.d.ts +13 -0
- package/dist/backend/services/adapters/orders.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/orders.adapter.js +48 -0
- package/dist/backend/services/adapters/orders.adapter.js.map +1 -0
- package/dist/backend/services/adapters/products.adapter.d.ts +16 -0
- package/dist/backend/services/adapters/products.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/products.adapter.js +88 -0
- package/dist/backend/services/adapters/products.adapter.js.map +1 -0
- package/dist/backend/services/adapters/stock.adapter.d.ts +15 -0
- package/dist/backend/services/adapters/stock.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/stock.adapter.js +62 -0
- package/dist/backend/services/adapters/stock.adapter.js.map +1 -0
- package/dist/backend/services/csv-codec.d.ts +17 -0
- package/dist/backend/services/csv-codec.d.ts.map +1 -0
- package/dist/backend/services/csv-codec.js +95 -0
- package/dist/backend/services/csv-codec.js.map +1 -0
- package/dist/backend/services/import-export-service.d.ts +52 -0
- package/dist/backend/services/import-export-service.d.ts.map +1 -0
- package/dist/backend/services/import-export-service.js +100 -0
- package/dist/backend/services/import-export-service.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +138 -0
- package/dist/manifest.js.map +1 -0
- package/docs/import_export.md +79 -0
- package/i18n/en.json +27 -0
- package/i18n/pl.json +27 -0
- package/package.json +82 -0
- 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 @@
|
|
|
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 @@
|
|
|
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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"}
|