diff --git a/app/__test__/helper.ts b/app/__test__/helper.ts index ae6454f..405e46f 100644 --- a/app/__test__/helper.ts +++ b/app/__test__/helper.ts @@ -4,6 +4,9 @@ import Database from "libsql"; import { format as sqlFormat } from "sql-formatter"; import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data"; import type { em as protoEm } from "../src/data/prototype"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { slugify } from "core/utils/strings"; export function getDummyDatabase(memory: boolean = true): { dummyDb: SqliteDatabase; @@ -71,3 +74,46 @@ export function schemaToEm(s: ReturnType, conn?: Connection): En export const assetsPath = `${import.meta.dir}/_assets`; export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`; + +export async function enableFetchLogging() { + const originalFetch = global.fetch; + + global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const response = await originalFetch(input, init); + const url = input instanceof URL || typeof input === "string" ? input : input.url; + + // Only clone if it's a supported content type + const contentType = response.headers.get("content-type") || ""; + const isSupported = + contentType.includes("json") || + contentType.includes("text") || + contentType.includes("xml"); + + if (isSupported) { + const clonedResponse = response.clone(); + let extension = "txt"; + let body: string; + + if (contentType.includes("json")) { + body = JSON.stringify(await clonedResponse.json(), null, 2); + extension = "json"; + } else if (contentType.includes("xml")) { + body = await clonedResponse.text(); + extension = "xml"; + } else { + body = await clonedResponse.text(); + } + + const fileName = `${new Date().getTime()}_${init?.method ?? "GET"}_${slugify(String(url))}.${extension}`; + const filePath = join(assetsTmpPath, fileName); + + await writeFile(filePath, body); + } + + return response; + }; + + return () => { + global.fetch = originalFetch; + }; +} diff --git a/app/__test__/media/adapters/StorageS3Adapter.spec.ts b/app/__test__/media/adapters/StorageS3Adapter.spec.ts index ad777e7..7b4a0a4 100644 --- a/app/__test__/media/adapters/StorageS3Adapter.spec.ts +++ b/app/__test__/media/adapters/StorageS3Adapter.spec.ts @@ -1,8 +1,9 @@ -import { describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { randomString } from "../../../src/core/utils"; import { StorageS3Adapter } from "../../../src/media"; import { config } from "dotenv"; +//import { enableFetchLogging } from "../../helper"; const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` }); const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } = dotenvOutput.parsed!; @@ -11,7 +12,17 @@ const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_ const ALL_TESTS = !!process.env.ALL_TESTS; console.log("ALL_TESTS?", ALL_TESTS); -describe.skipIf(true)("StorageS3Adapter", async () => { +/* +// @todo: preparation to mock s3 calls + replace fast-xml-parser +let cleanup: () => void; +beforeAll(async () => { + cleanup = await enableFetchLogging(); +}); +afterAll(() => { + cleanup(); +}); */ + +describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => { if (ALL_TESTS) return; const versions = [ @@ -66,7 +77,7 @@ describe.skipIf(true)("StorageS3Adapter", async () => { test.skipIf(disabled("putObject"))("puts an object", async () => { objects = (await adapter.listObjects()).length; - expect(await adapter.putObject(filename, file)).toBeString(); + expect(await adapter.putObject(filename, file as any)).toBeString(); }); test.skipIf(disabled("listObjects"))("lists objects", async () => { diff --git a/app/package.json b/app/package.json index e08d252..73614d7 100644 --- a/app/package.json +++ b/app/package.json @@ -46,7 +46,7 @@ "@xyflow/react": "^12.4.4", "aws4fetch": "^1.0.20", "dayjs": "^1.11.13", - "fast-xml-parser": "^4.4.0", + "fast-xml-parser": "^5.0.8", "hono": "^4.6.12", "json-schema-form-react": "^0.0.2", "json-schema-library": "^10.0.0-rc7", diff --git a/app/src/core/utils/strings.ts b/app/src/core/utils/strings.ts index a28d7ea..be115f5 100644 --- a/app/src/core/utils/strings.ts +++ b/app/src/core/utils/strings.ts @@ -118,3 +118,17 @@ export function patternMatch(target: string, pattern: RegExp | string): boolean } return false; } + +export function slugify(str: string): string { + return ( + String(str) + .normalize("NFKD") // split accented characters into their base characters and diacritical marks + // biome-ignore lint/suspicious/noMisleadingCharacterClass: + .replace(/[\u0300-\u036f]/g, "") // remove all the accents, which happen to be all in the \u03xx UNICODE block. + .trim() // trim leading or trailing whitespace + .toLowerCase() // convert to lowercase + .replace(/[^a-z0-9 -]/g, "") // remove non-alphanumeric characters + .replace(/\s+/g, "-") // replace spaces with hyphens + .replace(/-+/g, "-") // remove consecutive hyphens + ); +} diff --git a/bun.lock b/bun.lock index 4024166..cdc38c1 100644 --- a/bun.lock +++ b/bun.lock @@ -44,7 +44,7 @@ "@xyflow/react": "^12.4.4", "aws4fetch": "^1.0.20", "dayjs": "^1.11.13", - "fast-xml-parser": "^4.4.0", + "fast-xml-parser": "^5.0.8", "hono": "^4.6.12", "json-schema-form-react": "^0.0.2", "json-schema-library": "^10.0.0-rc7", @@ -1821,7 +1821,7 @@ "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], + "fast-xml-parser": ["fast-xml-parser@5.0.8", "", { "dependencies": { "strnum": "^2.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-qY8NiI5L8ff00F2giyICiJxSSKHO52tC36LJqx2JtvGyAd5ZfehC/l4iUVVHpmpIa6sM9N5mneSLHQG2INGoHA=="], "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], @@ -2965,7 +2965,7 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], + "strnum": ["strnum@2.0.5", "", {}, "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q=="], "style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="], @@ -4111,6 +4111,8 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@aws-sdk/core/fast-xml-parser/strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="],