@porulle/import-flat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @porulle/import-flat
2
+
3
+ Import catalog entities from a flat JSON file. The lowest-friction format — define an array of products, run `porulle import`, done.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ porulle import ./catalog.json
9
+ ```
10
+
11
+ Format:
12
+
13
+ ```json
14
+ [
15
+ {
16
+ "type": "product",
17
+ "slug": "ceylon-black-tea-50g",
18
+ "attributes": {
19
+ "locale": "en",
20
+ "title": "Ceylon Black Tea (50g)",
21
+ "description": "Single-estate, hand-picked.",
22
+ "subtitle": "Uva region"
23
+ },
24
+ "metadata": { "weight": 50, "material": "tea leaves" },
25
+ "variants": [
26
+ { "sku": "CBT-50G", "options": { "size": "50g" }, "price": 1200 }
27
+ ]
28
+ }
29
+ ]
30
+ ```
31
+
32
+ ## When to use
33
+
34
+ - Seeding a new store with a small fixed catalog
35
+ - Internal inventory dumps converted from a spreadsheet (export CSV → flatten to JSON)
36
+ - Test fixtures
37
+ - Anything that doesn't have a Shopify or WooCommerce origin
38
+
39
+ For Shopify migrations: `@porulle/import-shopify`. For WooCommerce: `@porulle/import-woocommerce`.
40
+
41
+ ## See also
42
+
43
+ - `@porulle/cli` — `porulle import` command
44
+ - `apps/store-example/src/scripts/seed.ts` — programmatic seeding example
@@ -0,0 +1,49 @@
1
+ import { type Result } from "@porulle/core";
2
+ export interface FlatImportEntityInput {
3
+ type: string;
4
+ slug: string;
5
+ attributes: {
6
+ locale?: string;
7
+ title: string;
8
+ description?: string;
9
+ subtitle?: string;
10
+ };
11
+ metadata?: Record<string, unknown>;
12
+ customFields?: Record<string, unknown>;
13
+ }
14
+ export interface FlatImportTarget {
15
+ createEntity(input: FlatImportEntityInput): Promise<{
16
+ id: string;
17
+ }>;
18
+ }
19
+ export type FlatValueResolver = string | ((row: Record<string, string>) => unknown);
20
+ export interface FlatImportMapping {
21
+ entityType: string;
22
+ slug: FlatValueResolver;
23
+ title: FlatValueResolver;
24
+ description?: FlatValueResolver;
25
+ subtitle?: FlatValueResolver;
26
+ locale?: string;
27
+ metadata?: Record<string, FlatValueResolver>;
28
+ customFields?: Record<string, FlatValueResolver>;
29
+ }
30
+ export interface ImportFlatOptions {
31
+ mapping: FlatImportMapping;
32
+ target: FlatImportTarget;
33
+ rows?: Array<Record<string, string>>;
34
+ csv?: string;
35
+ json?: string;
36
+ delimiter?: string;
37
+ }
38
+ export interface ImportFlatSummary {
39
+ imported: number;
40
+ failed: number;
41
+ errors: Array<{
42
+ row: number;
43
+ message: string;
44
+ }>;
45
+ entityIds: string[];
46
+ }
47
+ export declare function parseCsv(input: string, delimiter?: string): Array<Record<string, string>>;
48
+ export declare function importFlat(options: ImportFlatOptions): Promise<Result<ImportFlatSummary>>;
49
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE;QACV,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrE;AAED,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,OAAO,CAAC,CAAC;AAEpF,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,gBAAgB,CAAC;IACzB,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAiCD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAyBtF;AAqFD,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAoC/F"}
@@ -0,0 +1,154 @@
1
+ import { Err, Ok } from "@porulle/core";
2
+ function parseCsvLine(line, delimiter) {
3
+ const output = [];
4
+ let current = "";
5
+ let inQuotes = false;
6
+ for (let i = 0; i < line.length; i += 1) {
7
+ const char = line[i];
8
+ if (char === '"') {
9
+ const next = line[i + 1];
10
+ if (inQuotes && next === '"') {
11
+ current += '"';
12
+ i += 1;
13
+ continue;
14
+ }
15
+ inQuotes = !inQuotes;
16
+ continue;
17
+ }
18
+ if (char === delimiter && !inQuotes) {
19
+ output.push(current.trim());
20
+ current = "";
21
+ continue;
22
+ }
23
+ current += char;
24
+ }
25
+ output.push(current.trim());
26
+ return output;
27
+ }
28
+ export function parseCsv(input, delimiter = ",") {
29
+ const lines = input
30
+ .split(/\r?\n/)
31
+ .map((line) => line.trim())
32
+ .filter(Boolean);
33
+ if (lines.length === 0)
34
+ return [];
35
+ const headers = parseCsvLine(lines[0], delimiter);
36
+ const rows = [];
37
+ for (const line of lines.slice(1)) {
38
+ const values = parseCsvLine(line, delimiter);
39
+ const row = {};
40
+ for (let i = 0; i < headers.length; i += 1) {
41
+ const key = headers[i];
42
+ if (!key)
43
+ continue;
44
+ row[key] = values[i] ?? "";
45
+ }
46
+ rows.push(row);
47
+ }
48
+ return rows;
49
+ }
50
+ function resolveValue(row, resolver) {
51
+ if (!resolver)
52
+ return undefined;
53
+ if (typeof resolver === "function")
54
+ return resolver(row);
55
+ return row[resolver];
56
+ }
57
+ function mapRow(row, mapping) {
58
+ const slug = String(resolveValue(row, mapping.slug) ?? "").trim();
59
+ const title = String(resolveValue(row, mapping.title) ?? "").trim();
60
+ const metadata = mapping.metadata
61
+ ? Object.fromEntries(Object.entries(mapping.metadata)
62
+ .map(([key, resolver]) => [key, resolveValue(row, resolver)])
63
+ .filter(([, value]) => value !== undefined && value !== ""))
64
+ : undefined;
65
+ const customFields = mapping.customFields
66
+ ? Object.fromEntries(Object.entries(mapping.customFields)
67
+ .map(([key, resolver]) => [key, resolveValue(row, resolver)])
68
+ .filter(([, value]) => value !== undefined && value !== ""))
69
+ : undefined;
70
+ return {
71
+ type: mapping.entityType,
72
+ slug,
73
+ attributes: {
74
+ title,
75
+ ...(mapping.locale ? { locale: mapping.locale } : {}),
76
+ ...(resolveValue(row, mapping.description)
77
+ ? { description: String(resolveValue(row, mapping.description)) }
78
+ : {}),
79
+ ...(resolveValue(row, mapping.subtitle)
80
+ ? { subtitle: String(resolveValue(row, mapping.subtitle)) }
81
+ : {}),
82
+ },
83
+ ...(metadata && Object.keys(metadata).length > 0 ? { metadata } : {}),
84
+ ...(customFields && Object.keys(customFields).length > 0 ? { customFields } : {}),
85
+ };
86
+ }
87
+ function normalizeRows(options) {
88
+ if (options.rows && options.rows.length > 0)
89
+ return Ok(options.rows);
90
+ if (options.csv) {
91
+ return Ok(parseCsv(options.csv, options.delimiter ?? ","));
92
+ }
93
+ if (options.json) {
94
+ try {
95
+ const parsed = JSON.parse(options.json);
96
+ if (!Array.isArray(parsed)) {
97
+ return Err({
98
+ code: "IMPORT_FLAT_INVALID_JSON",
99
+ message: "JSON payload must be an array of records.",
100
+ });
101
+ }
102
+ const rows = parsed.map((value) => {
103
+ if (!value || typeof value !== "object")
104
+ return {};
105
+ return Object.fromEntries(Object.entries(value).map(([key, current]) => [key, String(current ?? "")]));
106
+ });
107
+ return Ok(rows);
108
+ }
109
+ catch (error) {
110
+ return Err({
111
+ code: "IMPORT_FLAT_INVALID_JSON",
112
+ message: error instanceof Error ? error.message : "Invalid JSON payload.",
113
+ });
114
+ }
115
+ }
116
+ return Err({
117
+ code: "IMPORT_FLAT_NO_INPUT",
118
+ message: "Provide rows, csv, or json input for flat import.",
119
+ });
120
+ }
121
+ export async function importFlat(options) {
122
+ const normalized = normalizeRows(options);
123
+ if (!normalized.ok)
124
+ return normalized;
125
+ const summary = {
126
+ imported: 0,
127
+ failed: 0,
128
+ errors: [],
129
+ entityIds: [],
130
+ };
131
+ for (let index = 0; index < normalized.value.length; index += 1) {
132
+ try {
133
+ const row = normalized.value[index];
134
+ const mapped = mapRow(row, options.mapping);
135
+ if (!mapped.slug) {
136
+ throw new Error("Missing slug after mapping.");
137
+ }
138
+ if (!mapped.attributes.title) {
139
+ throw new Error("Missing title after mapping.");
140
+ }
141
+ const created = await options.target.createEntity(mapped);
142
+ summary.imported += 1;
143
+ summary.entityIds.push(created.id);
144
+ }
145
+ catch (error) {
146
+ summary.failed += 1;
147
+ summary.errors.push({
148
+ row: index + 1,
149
+ message: error instanceof Error ? error.message : "Unknown import error.",
150
+ });
151
+ }
152
+ }
153
+ return Ok(summary);
154
+ }