diff --git a/README.md b/README.md index 44afebb..9c0bd22 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,20 @@ Check out the examples/ directory for ready-to-use JSON samples: ## 📦 Installation +Add an `.npmrc` file to your project: + +``` +@analtools:registry=https://npm.pkg.github.com +``` + +Then run one of the following commands: + ```sh -npm install jsonormalize +npm install @analtools/jsonormalize # or -yarn add jsonormalize +yarn add @analtools/jsonormalize # or -pnpm add jsonormalize +pnpm add @analtools/jsonormalize ``` ## 🚀 Quick Start diff --git a/package-lock.json b/package-lock.json index deec57e..4b886f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@analtools/jsonormalize", - "version": "0.0.6", + "version": "0.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@analtools/jsonormalize", - "version": "0.0.6", + "version": "0.0.12", "license": "MIT", "dependencies": { "@electric-sql/pglite": "^0.3.14", diff --git a/package.json b/package.json index e1690f9..2a06438 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@analtools/jsonormalize", - "version": "0.0.6", + "version": "0.0.12", "description": "JSONormalize — Transform any JSON into a relational database schema. Automatically normalizes nested structures, detects relationships, and generates SQLite migrations. Perfect for rapid prototyping, data migrations, and structured data workflows.", "keywords": [ "json-normalize", @@ -33,7 +33,8 @@ "data-formatting" ], "publishConfig": { - "registry": "https://npm.pkg.github.com" + "registry": "https://npm.pkg.github.com", + "access": "publics" }, "repository": { "type": "git", diff --git a/src/commands/postgres/setup.ts b/src/commands/postgres/setup.ts index 08c6144..cb07985 100644 --- a/src/commands/postgres/setup.ts +++ b/src/commands/postgres/setup.ts @@ -98,7 +98,7 @@ export async function setup( clientEncoding?: string; fallbackApplicationName?: string; options?: string; - }, + } = {}, ) { const config: ClientConfig | undefined = dbPath === undefined && Object.keys(options).length diff --git a/src/commands/prepare.ts b/src/commands/prepare.ts index a287476..0951422 100644 --- a/src/commands/prepare.ts +++ b/src/commands/prepare.ts @@ -19,8 +19,7 @@ async function getRawData(jsonPath: string): Promise { } export async function prepare(jsonPath: string) { - const rawData = await getRawData(jsonPath); - const data: unknown[] = Array.isArray(rawData) ? rawData : [rawData]; + const data = await getRawData(jsonPath); const jsonPathNameWithoutExt = path.basename( jsonPath, diff --git a/src/normalize/normalize-dictionaries.ts b/src/normalize/normalize-dictionaries.ts new file mode 100644 index 0000000..9908db7 --- /dev/null +++ b/src/normalize/normalize-dictionaries.ts @@ -0,0 +1,40 @@ +import { isDictionary } from "../utils"; +import type { NormalizedData } from "./types"; + +export function normalizeDictionaries(data: any): NormalizedData { + if (Array.isArray(data)) { + return data; + } else if (isDictionary(data)) { + const types = Array.from( + new Set( + Object.values(data) + .filter((value) => value !== null) + .map((value) => typeof value), + ), + ); + + if (types.length === 1) { + return Object.entries(data).map(([key, value]: [string, any]) => ({ + key, + value, + })); + } else { + const baseRow = Object.fromEntries( + types.map((type) => [`value_${type}`, null]), + ); + return Object.entries(data).map(([key, value]) => ({ + key, + ...baseRow, + ...(value === null + ? {} + : { + [`value_${typeof value}`]: value, + }), + })); + } + } else if (Object(data) === data) { + return [data]; + } else { + return []; + } +} diff --git a/src/normalize/normalize.test.ts b/src/normalize/normalize.test.ts index 00f2498..cc8ad5c 100644 --- a/src/normalize/normalize.test.ts +++ b/src/normalize/normalize.test.ts @@ -3,6 +3,83 @@ import { describe, expect, it } from "vitest"; import { normalize } from "./normalize"; describe("normalize", () => { + it("should normalize dictionary", () => { + expect( + normalize({ + a: "A", + b: "B", + c: null, + }), + ).toEqual([ + { key: "a", value: "A" }, + { key: "b", value: "B" }, + { key: "c", value: null }, + ]); + + expect( + normalize({ + a: 1, + b: null, + c: 3, + }), + ).toEqual([ + { key: "a", value: 1 }, + { key: "b", value: null }, + { key: "c", value: 3 }, + ]); + + expect( + normalize({ + a: null, + b: true, + c: false, + }), + ).toEqual([ + { key: "a", value: null }, + { key: "b", value: true }, + { key: "c", value: false }, + ]); + + expect( + normalize({ + a: 1, + b: "B", + c: true, + d: null, + }), + ).toEqual([ + { key: "a", value_number: 1, value_string: null, value_boolean: null }, + { key: "b", value_number: null, value_string: "B", value_boolean: null }, + { key: "c", value_number: null, value_string: null, value_boolean: true }, + { key: "d", value_number: null, value_string: null, value_boolean: null }, + ]); + + expect( + normalize({ + obj: { + sub: { + a: 1, + b: 2, + c: 3, + }, + }, + }), + ).toEqual([ + { + key: "obj_sub_a", + value: 1, + }, + { + key: "obj_sub_b", + value: 2, + }, + { + key: "obj_sub_c", + value: 3, + }, + ]); + }); + it("should flatten nested objects with localization and arrays", () => { expect( normalize([ diff --git a/src/normalize/normalize.ts b/src/normalize/normalize.ts index 71886fb..862de3a 100644 --- a/src/normalize/normalize.ts +++ b/src/normalize/normalize.ts @@ -1,18 +1,21 @@ import { normalizePrimitiveArrays } from "./normalize-array-of-arrays"; import { normalizeArrayOfPrimitives } from "./normalize-array-of-primitives"; import { normalizeDeepObjects } from "./normalize-deep-objects"; +import { normalizeDictionaries } from "./normalize-dictionaries"; import { normalizeLocalization } from "./normalize-localization"; import type { NormalizedData } from "./types"; -export function normalize(data: unknown[]): NormalizedData { - /* replace {a:{b:'c'}} to {'a_b':'c'} */ - return normalizeDeepObjects( - /* replace [[...],[...]] to [{items:[...]},{items:[...]}] */ - normalizePrimitiveArrays( - /* replace [1,2,3] to [{value:1},{value:2},{value:3}] */ - normalizeArrayOfPrimitives( - /* replace { en: string, zh: string, ... } to { lang: string, text: string }[]*/ - normalizeLocalization(data), +export function normalize(data: unknown): NormalizedData { + return normalizeDictionaries( + /* replace {a:{b:'c'}} to {'a_b':'c'} */ + normalizeDeepObjects( + /* replace [[...],[...]] to [{items:[...]},{items:[...]}] */ + normalizePrimitiveArrays( + /* replace [1,2,3] to [{value:1},{value:2},{value:3}] */ + normalizeArrayOfPrimitives( + /* replace { en: string, zh: string, ... } to { lang: string, text: string }[]*/ + normalizeLocalization(data), + ), ), ), ); diff --git a/src/postgres/create-migrations.ts b/src/postgres/create-migrations.ts index ea2ce48..ecb5994 100644 --- a/src/postgres/create-migrations.ts +++ b/src/postgres/create-migrations.ts @@ -8,7 +8,7 @@ export function createMigrations({ data, }: { prefix: string; - data: unknown[]; + data: unknown; }) { const tables = createRelationalStructure(prefix, normalize(data)); diff --git a/src/postgres/setup-tables.ts b/src/postgres/setup-tables.ts index ac9aba1..d2e30c9 100644 --- a/src/postgres/setup-tables.ts +++ b/src/postgres/setup-tables.ts @@ -12,7 +12,7 @@ export async function setupTables({ config?: ClientConfig; path?: string; prefix: string; - data: unknown[]; + data: unknown; }) { const { initialMigration, dataMigration } = createMigrations({ prefix, diff --git a/src/sqlite/create-migrations.ts b/src/sqlite/create-migrations.ts index ea2ce48..ecb5994 100644 --- a/src/sqlite/create-migrations.ts +++ b/src/sqlite/create-migrations.ts @@ -8,7 +8,7 @@ export function createMigrations({ data, }: { prefix: string; - data: unknown[]; + data: unknown; }) { const tables = createRelationalStructure(prefix, normalize(data)); diff --git a/src/sqlite/setup-tables.ts b/src/sqlite/setup-tables.ts index 8137180..2c60026 100644 --- a/src/sqlite/setup-tables.ts +++ b/src/sqlite/setup-tables.ts @@ -9,7 +9,7 @@ export function setupTables({ }: { path?: string; prefix: string; - data: unknown[]; + data: unknown; }) { const { initialMigration, dataMigration } = createMigrations({ prefix, diff --git a/src/utils/index.ts b/src/utils/index.ts index b7c814e..fd7a222 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,6 @@ export * from "./is-array-of-arrays"; export * from "./is-array-of-primitives"; +export * from "./is-dictionary"; export * from "./is-localization-object"; export * from "./is-url"; export * from "./snake-case"; diff --git a/src/utils/is-dictionary.ts b/src/utils/is-dictionary.ts new file mode 100644 index 0000000..33131f5 --- /dev/null +++ b/src/utils/is-dictionary.ts @@ -0,0 +1,10 @@ +const simpleTypes = ["string", "number", "boolean"]; + +export function isDictionary(data: any) { + return ( + Object(data) === data || + Object.values(data).every( + (value) => value === null || simpleTypes.includes(typeof value), + ) + ); +}