diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4b1304f..884875b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -41,5 +41,27 @@ jobs: uses: pnpm/action-setup@v4 - name: Install dependencies run: pnpm install - - name: Lint JS files + - name: Lint files run: pnpm run lint + Typecheck: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Restore node_modules cache + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-node-modules-${{ hashFiles('package.json') }} + restore-keys: | + ${{ runner.os }}-node-modules + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Install dependencies + run: pnpm install + - name: Typecheck files + run: pnpm run typecheck diff --git a/package.json b/package.json index baf0439..808b15c 100644 --- a/package.json +++ b/package.json @@ -23,17 +23,15 @@ "files": [ "dist" ], - "dependencies": { - "@ungap/structured-clone": "^1.0.1" - }, "devDependencies": { "prettier": "^3.3.3", "vite": "^6.3.5", "vitest": "^3.2.4" }, "scripts": { + "typecheck": "tsc --noEmit", "build": "vite build", - "lint": "prettier --check 'src/**/*.js'", + "lint": "prettier --check 'src/**/*'", "test": "vitest" }, "packageManager": "pnpm@10.12.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3b91ab..529d5f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,10 +7,6 @@ settings: importers: .: - dependencies: - '@ungap/structured-clone': - specifier: ^1.0.1 - version: 1.3.0 devDependencies: prettier: specifier: ^3.3.3 @@ -292,9 +288,6 @@ packages: '@types/node@24.0.3': resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -699,8 +692,6 @@ snapshots: undici-types: 7.8.0 optional: true - '@ungap/structured-clone@1.3.0': {} - '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 diff --git a/src/__tests__/alchemyApiDeserializer.spec.js b/src/__tests__/alchemyApiDeserializer.spec.js deleted file mode 100644 index 7674dd7..0000000 --- a/src/__tests__/alchemyApiDeserializer.spec.js +++ /dev/null @@ -1,515 +0,0 @@ -import { deserializePage, deserializePages } from "../alchemyApiDeserializer" - -describe("deserializePage", () => { - it("does not return any deprecated elements for single page", () => { - const pageData = { - data: { - type: "page", - id: "1", - attributes: { - name: "Homepage" - }, - relationships: { - elements: { - data: [ - { - type: "element", - id: "1" - }, - { - type: "element", - id: "2" - } - ] - } - } - }, - included: [ - { - type: "element", - id: "1", - attributes: { - name: "article", - deprecated: false - }, - relationships: { - essences: { - data: [ - { - id: "1", - type: "essence_text" - }, - { - id: "1", - type: "essence_picture" - } - ] - }, - nested_elements: { - data: [ - { - id: "3", - type: "element" - }, - { - id: "4", - type: "element" - } - ] - } - } - }, - { - type: "element", - id: "2", - attributes: { - name: "old", - deprecated: true - }, - relationships: {} - }, - { - type: "element", - id: "3", - attributes: { - name: "image", - deprecated: true - }, - relationships: {} - }, - { - type: "element", - id: "4", - attributes: { - name: "text", - deprecated: false - }, - relationships: {} - }, - { - type: "essence_text", - id: "1", - attributes: { - name: "text", - deprecated: true - } - }, - { - type: "essence_picture", - id: "1", - attributes: { - name: "image", - deprecated: false - } - } - ] - } - const page = deserializePage(pageData) - expect(page.elements).toEqual([ - { - id: "1", - name: "article", - deprecated: false, - essences: [ - { - id: "1", - name: "image", - deprecated: false - } - ], - nested_elements: [ - { - id: "4", - name: "text", - deprecated: false - } - ] - } - ]) - }) - - it("does not return any deprecated elements for single page with camelCased attributes", () => { - const pageData = { - data: { - type: "page", - id: "1", - attributes: { - name: "Homepage" - }, - relationships: { - elements: { - data: [ - { - type: "element", - id: "1" - }, - { - type: "element", - id: "2" - } - ] - } - } - }, - included: [ - { - type: "element", - id: "1", - attributes: { - name: "article", - deprecated: false - }, - relationships: { - essences: { - data: [ - { - id: "1", - type: "essence_text" - }, - { - id: "1", - type: "essence_picture" - } - ] - }, - nestedElements: { - data: [ - { - id: "3", - type: "element" - }, - { - id: "4", - type: "element" - } - ] - } - } - }, - { - type: "element", - id: "2", - attributes: { - name: "old", - deprecated: true - }, - relationships: {} - }, - { - type: "element", - id: "3", - attributes: { - name: "image", - deprecated: true - }, - relationships: {} - }, - { - type: "element", - id: "4", - attributes: { - name: "text", - deprecated: false - }, - relationships: {} - }, - { - type: "essence_text", - id: "1", - attributes: { - name: "text", - deprecated: true - } - }, - { - type: "essence_picture", - id: "1", - attributes: { - name: "image", - deprecated: false - } - } - ] - } - const page = deserializePage(pageData) - expect(page.elements).toEqual([ - { - id: "1", - name: "article", - deprecated: false, - essences: [ - { - id: "1", - name: "image", - deprecated: false - } - ], - nestedElements: [ - { - id: "4", - name: "text", - deprecated: false - } - ] - } - ]) - }) - - it("does not return any deprecated elements for pages", () => { - const pageData = { - data: [ - { - type: "page", - id: "1", - attributes: { - name: "Homepage" - }, - relationships: { - elements: { - data: [ - { - type: "element", - id: "1" - }, - { - type: "element", - id: "2" - } - ] - } - } - } - ], - included: [ - { - type: "element", - id: "1", - attributes: { - name: "article", - deprecated: false - }, - relationships: { - essences: { - data: [ - { - id: "1", - type: "essence_text" - }, - { - id: "1", - type: "essence_picture" - } - ] - }, - nested_elements: { - data: [ - { - id: "3", - type: "element" - }, - { - id: "4", - type: "element" - } - ] - } - } - }, - { - type: "element", - id: "2", - attributes: { - name: "old", - deprecated: true - }, - relationships: {} - }, - { - type: "element", - id: "3", - attributes: { - name: "image", - deprecated: true - }, - relationships: {} - }, - { - type: "element", - id: "4", - attributes: { - name: "text", - deprecated: false - }, - relationships: {} - }, - { - type: "essence_text", - id: "1", - attributes: { - name: "text", - deprecated: true - } - }, - { - type: "essence_picture", - id: "1", - attributes: { - name: "image", - deprecated: false - } - } - ] - } - const pages = deserializePages(pageData) - expect(pages[0].elements).toEqual([ - { - id: "1", - name: "article", - deprecated: false, - essences: [ - { - id: "1", - name: "image", - deprecated: false - } - ], - nested_elements: [ - { - id: "4", - name: "text", - deprecated: false - } - ] - } - ]) - }) - - it("does not return any deprecated elements for pages with camelCased attributes", () => { - const pageData = { - data: [ - { - type: "page", - id: "1", - attributes: { - name: "Homepage" - }, - relationships: { - elements: { - data: [ - { - type: "element", - id: "1" - }, - { - type: "element", - id: "2" - } - ] - } - } - } - ], - included: [ - { - type: "element", - id: "1", - attributes: { - name: "article", - deprecated: false - }, - relationships: { - essences: { - data: [ - { - id: "1", - type: "essence_text" - }, - { - id: "1", - type: "essence_picture" - } - ] - }, - nestedElements: { - data: [ - { - id: "3", - type: "element" - }, - { - id: "4", - type: "element" - } - ] - } - } - }, - { - type: "element", - id: "2", - attributes: { - name: "old", - deprecated: true - }, - relationships: {} - }, - { - type: "element", - id: "3", - attributes: { - name: "image", - deprecated: true - }, - relationships: {} - }, - { - type: "element", - id: "4", - attributes: { - name: "text", - deprecated: false - }, - relationships: {} - }, - { - type: "essence_text", - id: "1", - attributes: { - name: "text", - deprecated: true - } - }, - { - type: "essence_picture", - id: "1", - attributes: { - name: "image", - deprecated: false - } - } - ] - } - const pages = deserializePages(pageData) - expect(pages[0].elements).toEqual([ - { - id: "1", - name: "article", - deprecated: false, - essences: [ - { - id: "1", - name: "image", - deprecated: false - } - ], - nestedElements: [ - { - id: "4", - name: "text", - deprecated: false - } - ] - } - ]) - }) -}) diff --git a/src/__tests__/alchemyApiDeserializer.spec.ts b/src/__tests__/alchemyApiDeserializer.spec.ts new file mode 100644 index 0000000..4d3c6e6 --- /dev/null +++ b/src/__tests__/alchemyApiDeserializer.spec.ts @@ -0,0 +1,245 @@ +import { deserializePage, deserializePages } from "../alchemyApiDeserializer" +import { describe, it, expect } from "vitest" + +describe("deserializePage", () => { + it("does not return any deprecated elements for single page", () => { + const pageData = { + data: { + id: "1", + type: "page", + attributes: { + name: "Homepage" + }, + relationships: { + elements: { + data: [ + { + type: "element", + id: "1" + }, + { + type: "element", + id: "2" + } + ] + } + } + }, + included: [ + { + type: "element", + id: "1", + attributes: { + name: "article" + } + }, + { + type: "element", + id: "2", + attributes: { + name: "old", + deprecated: true + } + } + ] + } + const page = deserializePage(pageData) + expect(page).toEqual({ + name: "Homepage", + id: "1", + elements: [{ name: "article", id: "1" }] + }) + }) + + it("does not return any deprecated nestedElements for single page", () => { + const pageData = { + data: { + id: "1", + type: "page", + attributes: { + name: "Homepage" + }, + relationships: { + elements: { + data: [ + { + type: "element", + id: "1" + } + ] + } + } + }, + included: [ + { + id: "1", + type: "element", + attributes: { + name: "article" + }, + nestedElements: [ + { + id: "2", + type: "element" + }, + { + id: "3", + type: "element" + } + ] + }, + { + id: "2", + type: "element", + attributes: { + name: "old", + deprecated: true + } + }, + { + id: "3", + type: "element", + attributes: { + name: "title", + deprecated: false + } + } + ] + } + const page = deserializePage(pageData) + expect(page).toEqual({ + name: "Homepage", + id: "1", + elements: [{ name: "article", id: "1" }] + }) + }) + + it("does not return any deprecated elements for pages", () => { + const pageData = { + data: [ + { + type: "page", + id: "1", + attributes: { + name: "Homepage" + }, + relationships: { + elements: { + data: [ + { + type: "element", + id: "1" + }, + { + type: "element", + id: "2" + } + ] + } + } + } + ], + included: [ + { + type: "element", + id: "1", + attributes: { + name: "article", + deprecated: false + }, + relationships: { + ingredients: { + data: [ + { + id: "1", + type: "text" + }, + { + id: "1", + type: "picture" + } + ] + }, + nestedElements: { + data: [ + { + id: "3", + type: "element" + }, + { + id: "4", + type: "element" + } + ] + } + } + }, + { + type: "element", + id: "2", + attributes: { + name: "old", + deprecated: true + }, + relationships: {} + }, + { + type: "element", + id: "3", + attributes: { + name: "image", + deprecated: true + }, + relationships: {} + }, + { + type: "element", + id: "4", + attributes: { + name: "text", + deprecated: false + }, + relationships: {} + }, + { + type: "text", + id: "1", + attributes: { + name: "text", + deprecated: true + } + }, + { + type: "picture", + id: "1", + attributes: { + name: "image", + deprecated: false + } + } + ] + } + const pages = deserializePages(pageData) + expect(pages[0].elements).toEqual([ + { + id: "1", + name: "article", + deprecated: false, + ingredients: [ + { + id: "1", + name: "image", + deprecated: false + } + ], + nestedElements: [ + { + id: "4", + name: "text", + deprecated: false + } + ] + } + ]) + }) +}) diff --git a/src/__tests__/deserialize.spec.js b/src/__tests__/deserialize.spec.ts similarity index 91% rename from src/__tests__/deserialize.spec.js rename to src/__tests__/deserialize.spec.ts index cb8c3fc..bfa3cda 100644 --- a/src/__tests__/deserialize.spec.js +++ b/src/__tests__/deserialize.spec.ts @@ -1,4 +1,5 @@ import { deserialize } from "../deserialize" +import { describe, it, expect } from "vitest" describe("deserialize", () => { it("Complex serialize", () => { @@ -38,7 +39,7 @@ describe("deserialize", () => { ] } - expect(deserialize(serialized)).toEqual([ + expect(deserialize(serialized as any)).toEqual([ { id: 1, "first-name": "Joe", diff --git a/src/alchemyApiDeserializer.js b/src/alchemyApiDeserializer.js deleted file mode 100644 index f87db13..0000000 --- a/src/alchemyApiDeserializer.js +++ /dev/null @@ -1,43 +0,0 @@ -import { deserialize } from "./deserialize" - -// Recursively filters all deprecated elements and essences from collection -function filterDeprecatedElements(elements) { - const els = [] - - elements.forEach((element) => { - if (element.nested_elements?.length > 0) { - element.nested_elements = filterDeprecatedElements( - element.nested_elements - ) - } - if (element.nestedElements?.length > 0) { - element.nestedElements = filterDeprecatedElements(element.nestedElements) - } - if (element.essences?.length > 0) { - element.essences = element.essences.filter((essence) => { - return !essence.deprecated - }) - } - if (!element.deprecated) { - els.push(element) - } - }) - - return els -} - -// Returns deserialized page without deprecated content -export function deserializePage(pageData) { - const page = deserialize(pageData) - page.elements = filterDeprecatedElements(page.elements) - return page -} - -// Returns deserialized pages without deprecated content -export function deserializePages(pagesData) { - const pages = deserialize(pagesData) - pages.forEach((page) => { - page.elements = filterDeprecatedElements(page.elements) - }) - return pages -} diff --git a/src/alchemyApiDeserializer.ts b/src/alchemyApiDeserializer.ts new file mode 100644 index 0000000..2c5acfc --- /dev/null +++ b/src/alchemyApiDeserializer.ts @@ -0,0 +1,40 @@ +import { deserialize } from "./deserialize" + +// Recursively filters all deprecated elements and essences from collection +function filterDeprecatedElements(elements: AlchemyElement[]) { + return elements + .map((element) => { + if (element.nestedElements?.length > 0) { + element.nestedElements = filterDeprecatedElements( + element.nestedElements + ) + } + + if (element.ingredients?.length > 0) { + element.ingredients = element.ingredients.filter((ingredients) => { + return !ingredients.deprecated + }) + } + + if (!element.deprecated) { + return element + } + }) + .filter((element) => element !== undefined) +} + +// Returns deserialized page without deprecated content +export function deserializePage(pageData: JsonApiResponse) { + const page = deserialize(pageData) + page.elements = filterDeprecatedElements(page.elements) + return page +} + +// Returns deserialized pages without deprecated content +export function deserializePages(pagesData: JsonApiResponse) { + const pages = deserialize(pagesData) as AlchemyPage[] + pages.forEach((page) => { + page.elements = filterDeprecatedElements(page.elements) + }) + return pages +} diff --git a/src/deserialize.js b/src/deserialize.js deleted file mode 100644 index a1edacc..0000000 --- a/src/deserialize.js +++ /dev/null @@ -1,94 +0,0 @@ -import structuredClone from "@ungap/structured-clone" - -export function deserialize(originalResponse, options = {}) { - const response = structuredClone(originalResponse) - if (!options) { - options = {} - } - - const included = response.included || [] - - if (Array.isArray(response.data)) { - return response.data.map((data) => { - return parseJsonApiSimpleResourceData(data, included, false, options) - }) - } else { - return parseJsonApiSimpleResourceData( - response.data, - included, - false, - options - ) - } -} - -function parseJsonApiSimpleResourceData(data, included, useCache, options) { - if (!included.cached) { - included.cached = {} - } - - if (!(data.type in included.cached)) { - included.cached[data.type] = {} - } - - if (useCache && data.id in included.cached[data.type]) { - return included.cached[data.type][data.id] - } - - const attributes = data.attributes || {} - - const resource = attributes - resource.id = data.id - - included.cached[data.type][data.id] = resource - - if (data.relationships) { - for (const relationName of Object.keys(data.relationships)) { - const relationRef = data.relationships[relationName] - - if (Array.isArray(relationRef.data)) { - const items = [] - - relationRef.data.forEach((relationData) => { - const item = findJsonApiIncluded( - included, - relationData.type, - relationData.id, - options - ) - - items.push(item) - }) - - resource[relationName] = items - } else if (relationRef && relationRef.data) { - resource[relationName] = findJsonApiIncluded( - included, - relationRef.data.type, - relationRef.data.id, - options - ) - } else { - resource[relationName] = null - } - } - } - - return resource -} - -function findJsonApiIncluded(included, type, id, options) { - let found = null - - included.forEach((item) => { - if (item.type === type && item.id === id) { - found = parseJsonApiSimpleResourceData(item, included, true, options) - } - }) - - if (!found) { - found = { id } - } - - return found -} diff --git a/src/deserialize.ts b/src/deserialize.ts new file mode 100644 index 0000000..15e27c4 --- /dev/null +++ b/src/deserialize.ts @@ -0,0 +1,81 @@ +export function deserialize(originalResponse: JsonApiResponse) { + const response = structuredClone(originalResponse) + + const included = response.included || [] + + if (Array.isArray(response.data)) { + return response.data.map((data) => { + return parseJsonApiSimpleResourceData(data, included, false) + }) + } else { + return parseJsonApiSimpleResourceData(response.data, included, false) + } +} + +function parseJsonApiSimpleResourceData( + ressourceData: JsonApiRessource, + included: any, + useCache: any +) { + if (!included.cached) { + included.cached = {} + } + + if (!(ressourceData.type in included.cached)) { + included.cached[ressourceData.type] = {} + } + + if (useCache && ressourceData.id in included.cached[ressourceData.type]) { + return included.cached[ressourceData.type][ressourceData.id] + } + + const attributes = ressourceData.attributes || {} + + const resource = attributes + resource.id = ressourceData.id + + included.cached[ressourceData.type][ressourceData.id] = resource + + if (ressourceData.relationships) { + for (const relationName of Object.keys(ressourceData.relationships)) { + const relationRef = ressourceData.relationships[relationName] + + if (Array.isArray(relationRef.data)) { + const items = relationRef.data.map((relationData) => + findJsonApiIncluded(included, relationData.type, relationData.id) + ) + resource[relationName] = items + } else if (relationRef && relationRef.data) { + resource[relationName] = findJsonApiIncluded( + included, + relationRef.data.type, + relationRef.data.id + ) + } else { + resource[relationName] = null + } + } + } + + return resource +} + +function findJsonApiIncluded( + included: JsonApiRessource[], + type: string, + id: string +) { + let found = null + + included.forEach((item) => { + if (item.type === type && item.id === id) { + found = parseJsonApiSimpleResourceData(item, included, true) + } + }) + + if (!found) { + found = { id } + } + + return found +} diff --git a/src/main.d.ts b/src/main.d.ts new file mode 100644 index 0000000..f24551c --- /dev/null +++ b/src/main.d.ts @@ -0,0 +1,37 @@ +interface JsonApiRessource { + id: string + type: string + attributes?: { + [key: string]: any + } + relationships?: { + [key: string]: { data: JsonApiRessource | JsonApiRessource[] } + } +} + +interface JsonApiResponse { + data: JsonApiRessource | JsonApiRessource[] + included?: JsonApiRessource[] +} + +interface AlchemyElement { + id: string + name: string + deprecated?: boolean + ingredients: AlchemyIngredient[] + nestedElements?: AlchemyElement[] +} + +interface AlchemyPage { + id: string + elements: AlchemyElement[] + allElements: AlchemyElement[] + fixedElements?: AlchemyElement[] +} + +interface AlchemyIngredient { + id: string + role: string + element: AlchemyElement + deprecated?: boolean +} diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 3ed4a63..0000000 --- a/src/main.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./alchemyApiDeserializer.js" -export * from "./deserialize.js" diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..4833862 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,2 @@ +export * from "./alchemyApiDeserializer" +export * from "./deserialize" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8d03be6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "removeComments": true, + "noImplicitAny": true, + "skipLibCheck": true + }, + "files": ["src/main.d.ts"], + "include": ["src/**/*"] +} diff --git a/vite.config.ts b/vite.config.ts index d678d95..10e9ee2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,6 @@ import { defineConfig } from "vite" import { resolve } from "path" -const external = ["@ungap/structured-clone"] - export default defineConfig({ build: { lib: { @@ -12,9 +10,6 @@ export default defineConfig({ }, formats: ["es"], fileName: (_format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external } } }) diff --git a/vitest.config.mjs b/vitest.config.mjs index ad3b8df..90f9f26 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -2,6 +2,6 @@ import { defineConfig } from "vitest/config" export default defineConfig({ test: { - globals: true + globals: false } })