diff --git a/dataset/file/copy.spec.ts b/dataset/file/copy.spec.ts new file mode 100644 index 00000000..b1cd24af --- /dev/null +++ b/dataset/file/copy.spec.ts @@ -0,0 +1,134 @@ +import * as fs from "node:fs/promises" +import * as path from "node:path" +import { temporaryDirectory } from "tempy" +import { afterEach, beforeEach, describe, expect, it } from "vitest" +import { copyFile } from "./copy.ts" +import { writeTempFile } from "./temp.ts" + +describe("copyFile", () => { + let testDir: string + + beforeEach(() => { + testDir = temporaryDirectory() + }) + + afterEach(async () => { + try { + await fs.rm(testDir, { recursive: true, force: true }) + } catch (error) { + if (error instanceof Error && !error.message.includes("ENOENT")) { + console.error(`Failed to clean up test directory: ${testDir}`, error) + } + } + }) + + it("should copy file from source to target", async () => { + const sourcePath = await writeTempFile("test content") + const targetPath = path.join(testDir, "target.txt") + + await copyFile({ sourcePath, targetPath }) + + const fileExists = await fs + .stat(targetPath) + .then(() => true) + .catch(() => false) + expect(fileExists).toBe(true) + + const content = await fs.readFile(targetPath, "utf-8") + expect(content).toBe("test content") + }) + + it("should copy file with exact content", async () => { + const content = "Hello, World! This is a test file." + const sourcePath = await writeTempFile(content) + const targetPath = path.join(testDir, "copy.txt") + + await copyFile({ sourcePath, targetPath }) + + const copiedContent = await fs.readFile(targetPath, "utf-8") + expect(copiedContent).toBe(content) + }) + + it("should copy binary file", async () => { + const binaryData = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]) + const sourcePath = await writeTempFile(binaryData) + const targetPath = path.join(testDir, "binary.bin") + + await copyFile({ sourcePath, targetPath }) + + const copiedData = await fs.readFile(targetPath) + expect(Buffer.compare(copiedData, binaryData)).toBe(0) + }) + + it("should copy empty file", async () => { + const sourcePath = await writeTempFile("") + const targetPath = path.join(testDir, "empty.txt") + + await copyFile({ sourcePath, targetPath }) + + const content = await fs.readFile(targetPath, "utf-8") + expect(content).toBe("") + }) + + it("should copy large file", async () => { + const largeContent = "x".repeat(100000) + const sourcePath = await writeTempFile(largeContent) + const targetPath = path.join(testDir, "large.txt") + + await copyFile({ sourcePath, targetPath }) + + const copiedContent = await fs.readFile(targetPath, "utf-8") + expect(copiedContent).toBe(largeContent) + expect(copiedContent.length).toBe(100000) + }) + + it("should copy file with special characters", async () => { + const content = "Special characters: é, ñ, ü, ö, à, 中文, 日本語" + const sourcePath = await writeTempFile(content) + const targetPath = path.join(testDir, "special.txt") + + await copyFile({ sourcePath, targetPath }) + + const copiedContent = await fs.readFile(targetPath, "utf-8") + expect(copiedContent).toBe(content) + }) + + it("should copy file to nested directory", async () => { + const sourcePath = await writeTempFile("nested content") + const targetPath = path.join(testDir, "nested", "dir", "file.txt") + + await copyFile({ sourcePath, targetPath }) + + const fileExists = await fs + .stat(targetPath) + .then(() => true) + .catch(() => false) + expect(fileExists).toBe(true) + + const content = await fs.readFile(targetPath, "utf-8") + expect(content).toBe("nested content") + }) + + it("should copy json file", async () => { + const jsonContent = JSON.stringify({ name: "test", value: 123 }) + const sourcePath = await writeTempFile(jsonContent) + const targetPath = path.join(testDir, "data.json") + + await copyFile({ sourcePath, targetPath }) + + const copiedContent = await fs.readFile(targetPath, "utf-8") + expect(copiedContent).toBe(jsonContent) + expect(JSON.parse(copiedContent)).toEqual({ name: "test", value: 123 }) + }) + + it("should copy file with newlines", async () => { + const content = "Line 1\nLine 2\nLine 3\n" + const sourcePath = await writeTempFile(content) + const targetPath = path.join(testDir, "multiline.txt") + + await copyFile({ sourcePath, targetPath }) + + const copiedContent = await fs.readFile(targetPath, "utf-8") + expect(copiedContent).toBe(content) + }) +}) diff --git a/dataset/plugins/descriptor/plugin.spec.ts b/dataset/plugins/descriptor/plugin.spec.ts new file mode 100644 index 00000000..b2676ce0 --- /dev/null +++ b/dataset/plugins/descriptor/plugin.spec.ts @@ -0,0 +1,227 @@ +import type { Package } from "@dpkit/metadata" +import * as metadataModule from "@dpkit/metadata" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { DescriptorPlugin } from "./plugin.ts" + +vi.mock("@dpkit/metadata", async () => { + const actual = await vi.importActual("@dpkit/metadata") + return { + ...actual, + loadPackageDescriptor: vi.fn(), + savePackageDescriptor: vi.fn(), + } +}) + +describe("DescriptorPlugin", () => { + let plugin: DescriptorPlugin + let mockLoadPackageDescriptor: ReturnType + let mockSavePackageDescriptor: ReturnType + + beforeEach(() => { + plugin = new DescriptorPlugin() + mockLoadPackageDescriptor = vi.mocked(metadataModule.loadPackageDescriptor) + mockSavePackageDescriptor = vi.mocked(metadataModule.savePackageDescriptor) + vi.clearAllMocks() + }) + + describe("loadPackage", () => { + it("should load package from local datapackage.json file", async () => { + const mockPackage: Package = { + name: "test-package", + resources: [{ name: "test", data: [] }], + } + mockLoadPackageDescriptor.mockResolvedValue(mockPackage) + + const result = await plugin.loadPackage("./datapackage.json") + + expect(mockLoadPackageDescriptor).toHaveBeenCalledWith( + "./datapackage.json", + ) + expect(result).toEqual(mockPackage) + }) + + it("should load package from local json file", async () => { + const mockPackage: Package = { + name: "test-package", + resources: [{ name: "test", data: [] }], + } + mockLoadPackageDescriptor.mockResolvedValue(mockPackage) + + const result = await plugin.loadPackage("./package.json") + + expect(mockLoadPackageDescriptor).toHaveBeenCalledWith("./package.json") + expect(result).toEqual(mockPackage) + }) + + it("should return undefined for remote json urls", async () => { + const result = await plugin.loadPackage( + "https://example.com/datapackage.json", + ) + + expect(mockLoadPackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should return undefined for http remote json urls", async () => { + const result = await plugin.loadPackage( + "http://example.com/datapackage.json", + ) + + expect(mockLoadPackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should return undefined for local csv files", async () => { + const result = await plugin.loadPackage("./data.csv") + + expect(mockLoadPackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should return undefined for local xlsx files", async () => { + const result = await plugin.loadPackage("./data.xlsx") + + expect(mockLoadPackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should return undefined for local parquet files", async () => { + const result = await plugin.loadPackage("./data.parquet") + + expect(mockLoadPackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should handle absolute paths", async () => { + const mockPackage: Package = { + name: "test-package", + resources: [{ name: "test", data: [] }], + } + mockLoadPackageDescriptor.mockResolvedValue(mockPackage) + + const result = await plugin.loadPackage("/absolute/path/datapackage.json") + + expect(mockLoadPackageDescriptor).toHaveBeenCalledWith( + "/absolute/path/datapackage.json", + ) + expect(result).toEqual(mockPackage) + }) + + it("should return undefined for github urls", async () => { + const result = await plugin.loadPackage( + "https://github.com/owner/repo/datapackage.json", + ) + + expect(mockLoadPackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should return undefined for zenodo urls", async () => { + const result = await plugin.loadPackage("https://zenodo.org/record/123") + + expect(mockLoadPackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + }) + + describe("savePackage", () => { + const mockPackage: Package = { + name: "test-package", + resources: [{ name: "test", data: [] }], + } + + it("should save package to local datapackage.json file", async () => { + mockSavePackageDescriptor.mockResolvedValue(undefined) + + const result = await plugin.savePackage(mockPackage, { + target: "./datapackage.json", + }) + + expect(mockSavePackageDescriptor).toHaveBeenCalledWith(mockPackage, { + path: "./datapackage.json", + }) + expect(result).toEqual({ path: "./datapackage.json" }) + }) + + it("should save package with absolute path", async () => { + mockSavePackageDescriptor.mockResolvedValue(undefined) + + const result = await plugin.savePackage(mockPackage, { + target: "/absolute/path/datapackage.json", + }) + + expect(mockSavePackageDescriptor).toHaveBeenCalledWith(mockPackage, { + path: "/absolute/path/datapackage.json", + }) + expect(result).toEqual({ path: "/absolute/path/datapackage.json" }) + }) + + it("should return undefined for remote urls", async () => { + const result = await plugin.savePackage(mockPackage, { + target: "https://example.com/datapackage.json", + }) + + expect(mockSavePackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should return undefined for local json files not named datapackage.json", async () => { + const result = await plugin.savePackage(mockPackage, { + target: "./package.json", + }) + + expect(mockSavePackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should return undefined for local csv files", async () => { + const result = await plugin.savePackage(mockPackage, { + target: "./data.csv", + }) + + expect(mockSavePackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should return undefined for local xlsx files", async () => { + const result = await plugin.savePackage(mockPackage, { + target: "./data.xlsx", + }) + + expect(mockSavePackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should return undefined for http urls", async () => { + const result = await plugin.savePackage(mockPackage, { + target: "http://example.com/datapackage.json", + }) + + expect(mockSavePackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + + it("should ignore withRemote option for local files", async () => { + mockSavePackageDescriptor.mockResolvedValue(undefined) + + const result = await plugin.savePackage(mockPackage, { + target: "./datapackage.json", + withRemote: true, + }) + + expect(mockSavePackageDescriptor).toHaveBeenCalledWith(mockPackage, { + path: "./datapackage.json", + }) + expect(result).toEqual({ path: "./datapackage.json" }) + }) + + it("should return undefined for local directories", async () => { + const result = await plugin.savePackage(mockPackage, { + target: "./data", + }) + + expect(mockSavePackageDescriptor).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) + }) +}) diff --git a/dataset/plugins/github/package/convert/toGithub.ts b/dataset/plugins/github/package/convert/toGithub.ts deleted file mode 100644 index c0246cba..00000000 --- a/dataset/plugins/github/package/convert/toGithub.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Package } from "@dpkit/metadata" -import type { GithubPackage } from "../Package.ts" - -export function convertPackageToGithub(dataPackage: Package) { - const repoPayload: Partial & { - auto_init?: boolean - has_issues?: boolean - has_projects?: boolean - has_wiki?: boolean - } = { - name: dataPackage.name, - private: false, - auto_init: true, - has_issues: true, - has_projects: true, - has_wiki: true, - } - - if (dataPackage.description) { - repoPayload.description = dataPackage.description - } else if (dataPackage.title) { - repoPayload.description = dataPackage.title - } - - if (dataPackage.homepage) { - repoPayload.homepage = dataPackage.homepage - } - - if (dataPackage.keywords && dataPackage.keywords.length > 0) { - repoPayload.topics = dataPackage.keywords - } - - return repoPayload -} diff --git a/dataset/plugins/github/package/index.ts b/dataset/plugins/github/package/index.ts index 818e9c5a..ba2309ad 100644 --- a/dataset/plugins/github/package/index.ts +++ b/dataset/plugins/github/package/index.ts @@ -4,4 +4,3 @@ export type { GithubLicense } from "./License.ts" export { loadPackageFromGithub } from "./load.ts" export { savePackageToGithub } from "./save.ts" export { convertPackageFromGithub } from "./convert/fromGithub.ts" -export { convertPackageToGithub } from "./convert/toGithub.ts" diff --git a/library/table/infer.spec.ts b/library/table/infer.spec.ts new file mode 100644 index 00000000..5dcc7989 --- /dev/null +++ b/library/table/infer.spec.ts @@ -0,0 +1,227 @@ +import { writeTempFile } from "@dpkit/dataset" +import { describe, expect, it } from "vitest" +import { inferTable } from "./infer.ts" + +describe("inferTable", () => { + it("should infer table with dialect and schema from CSV", async () => { + const csvPath = await writeTempFile("id,name\n1,alice\n2,bob") + const resource = { path: csvPath, format: "csv" as const } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.dialect).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should use provided dialect when available", async () => { + const csvPath = await writeTempFile("id|name\n1|alice\n2|bob") + const resource = { + path: csvPath, + format: "csv" as const, + dialect: { delimiter: "|" }, + } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.dialect).toBeDefined() + expect(result?.dialect.delimiter).toBe("|") + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should use provided schema when available", async () => { + const csvPath = await writeTempFile("id,name\n1,alice\n2,bob") + const resource = { + path: csvPath, + format: "csv" as const, + schema: { + fields: [ + { name: "id", type: "integer" as const }, + { name: "name", type: "string" as const }, + ], + }, + } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.dialect).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.schema.fields).toHaveLength(2) + expect(result?.table).toBeDefined() + }) + + it("should infer table with custom delimiter", async () => { + const csvPath = await writeTempFile("id;name;age\n1;alice;25\n2;bob;30") + const resource = { + path: csvPath, + format: "csv" as const, + dialect: { delimiter: ";" }, + } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.dialect).toBeDefined() + expect(result?.dialect.delimiter).toBe(";") + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should infer table from inline data", async () => { + const resource = { + name: "test-resource", + type: "table" as const, + data: [ + { id: 1, name: "alice" }, + { id: 2, name: "bob" }, + ], + } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.dialect).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should infer schema with various data types", async () => { + const csvPath = await writeTempFile( + "id,name,score,active\n1,alice,95.5,true\n2,bob,87.3,false", + ) + const resource = { path: csvPath, format: "csv" as const } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.schema.fields).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should infer table with numeric columns", async () => { + const csvPath = await writeTempFile("id,score\n1,95.5\n2,87.3\n3,100") + const resource = { path: csvPath, format: "csv" as const } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.schema.fields).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should infer table with date columns", async () => { + const csvPath = await writeTempFile( + "id,created\n1,2024-01-01\n2,2024-01-02", + ) + const resource = { path: csvPath, format: "csv" as const } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should infer table with boolean columns", async () => { + const csvPath = await writeTempFile("id,active\n1,true\n2,false\n3,true") + const resource = { path: csvPath, format: "csv" as const } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should handle table with quoted fields", async () => { + const csvPath = await writeTempFile( + 'id,name,description\n1,"alice","Test, data"\n2,"bob","Normal"', + ) + const resource = { path: csvPath, format: "csv" as const } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.dialect).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should infer table with single row", async () => { + const csvPath = await writeTempFile("id,name\n1,alice") + const resource = { path: csvPath, format: "csv" as const } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.dialect).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should infer table with empty values", async () => { + const csvPath = await writeTempFile("id,name\n1,alice\n2,") + const resource = { path: csvPath, format: "csv" as const } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) + + it("should infer table with headers only", async () => { + const csvPath = await writeTempFile("id,name,age") + const resource = { path: csvPath, format: "csv" as const } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + }) + + it("should use both provided dialect and schema", async () => { + const csvPath = await writeTempFile("id|name\n1|alice\n2|bob") + const resource = { + path: csvPath, + format: "csv" as const, + dialect: { delimiter: "|" }, + schema: { + fields: [ + { name: "id", type: "integer" as const }, + { name: "name", type: "string" as const }, + ], + }, + } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.dialect).toBeDefined() + expect(result?.dialect.delimiter).toBe("|") + expect(result?.schema).toBeDefined() + expect(result?.schema.fields).toHaveLength(2) + expect(result?.table).toBeDefined() + }) + + it("should infer table with tab delimiter", async () => { + const csvPath = await writeTempFile("id\tname\n1\talice\n2\tbob") + const resource = { + path: csvPath, + format: "csv" as const, + dialect: { delimiter: "\t" }, + } + + const result = await inferTable(resource) + + expect(result).toBeDefined() + expect(result?.dialect).toBeDefined() + expect(result?.schema).toBeDefined() + expect(result?.table).toBeDefined() + }) +}) diff --git a/metadata/field/convert/fromDescriptor.spec.ts b/metadata/field/convert/fromDescriptor.spec.ts new file mode 100644 index 00000000..423f257a --- /dev/null +++ b/metadata/field/convert/fromDescriptor.spec.ts @@ -0,0 +1,305 @@ +import { describe, expect, it, vi } from "vitest" +import { convertFieldFromDescriptor } from "./fromDescriptor.ts" + +describe("convertFieldFromDescriptor", () => { + it("should return a cloned descriptor", () => { + const descriptor = { name: "id", type: "string" } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result).toEqual(descriptor) + expect(result).not.toBe(descriptor) + }) + + describe("format conversion", () => { + it("should remove fmt: prefix from format", () => { + const descriptor = { name: "id", type: "string", format: "fmt:email" } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.format).toBe("email") + }) + + it("should preserve format without fmt: prefix", () => { + const descriptor = { name: "id", type: "string", format: "email" } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.format).toBe("email") + }) + + it("should handle descriptor without format", () => { + const descriptor = { name: "id", type: "string" } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.format).toBeUndefined() + }) + }) + + describe("missingValues validation", () => { + it("should preserve valid missingValues array", () => { + const descriptor = { + name: "id", + type: "string", + missingValues: ["", "NA", "N/A"], + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.missingValues).toEqual(["", "NA", "N/A"]) + }) + + it("should remove invalid non-array missingValues and warn", () => { + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => {}) + const descriptor = { + name: "id", + type: "string", + missingValues: "invalid", + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.missingValues).toBeUndefined() + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Ignoring v2.0 incompatible missingValues: invalid", + ) + consoleWarnSpy.mockRestore() + }) + + it("should handle descriptor without missingValues", () => { + const descriptor = { name: "id", type: "string" } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.missingValues).toBeUndefined() + }) + }) + + describe("categories validation", () => { + it("should preserve valid categories array", () => { + const descriptor = { + name: "status", + type: "string", + categories: ["active", "inactive", "pending"], + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.categories).toEqual(["active", "inactive", "pending"]) + }) + + it("should remove invalid non-array categories and warn", () => { + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => {}) + const descriptor = { + name: "status", + type: "string", + categories: "invalid", + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.categories).toBeUndefined() + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Ignoring v2.0 incompatible categories: invalid", + ) + consoleWarnSpy.mockRestore() + }) + + it("should handle descriptor without categories", () => { + const descriptor = { name: "status", type: "string" } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.categories).toBeUndefined() + }) + }) + + describe("categoriesOrdered validation", () => { + it("should preserve valid categoriesOrdered boolean", () => { + const descriptor = { + name: "status", + type: "string", + categoriesOrdered: true, + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.categoriesOrdered).toBe(true) + }) + + it("should remove invalid non-boolean categoriesOrdered and warn", () => { + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => {}) + const descriptor = { + name: "status", + type: "string", + categoriesOrdered: "invalid", + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.categoriesOrdered).toBeUndefined() + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Ignoring v2.0 incompatible categoriesOrdered: invalid", + ) + consoleWarnSpy.mockRestore() + }) + + it("should handle descriptor without categoriesOrdered", () => { + const descriptor = { name: "status", type: "string" } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.categoriesOrdered).toBeUndefined() + }) + }) + + describe("jsonschema validation", () => { + it("should preserve valid jsonschema object", () => { + const descriptor = { + name: "data", + type: "object", + jsonschema: { type: "object", properties: { id: { type: "number" } } }, + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.jsonschema).toEqual({ + type: "object", + properties: { id: { type: "number" } }, + }) + }) + + it("should remove invalid non-object jsonschema and warn", () => { + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => {}) + const descriptor = { + name: "data", + type: "object", + jsonschema: "invalid", + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.jsonschema).toBeUndefined() + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Ignoring v2.0 incompatible jsonschema: invalid", + ) + consoleWarnSpy.mockRestore() + }) + + it("should handle descriptor without jsonschema", () => { + const descriptor = { name: "data", type: "object" } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.jsonschema).toBeUndefined() + }) + }) + + describe("combined conversions", () => { + it("should apply all conversions together", () => { + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => {}) + const descriptor = { + name: "email", + type: "string", + format: "fmt:email", + missingValues: ["", "NA"], + categories: ["valid", "invalid"], + categoriesOrdered: true, + jsonschema: { type: "string", pattern: "^[a-z]+@[a-z]+\\.[a-z]+$" }, + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.format).toBe("email") + expect(result.missingValues).toEqual(["", "NA"]) + expect(result.categories).toEqual(["valid", "invalid"]) + expect(result.categoriesOrdered).toBe(true) + expect(result.jsonschema).toEqual({ + type: "string", + pattern: "^[a-z]+@[a-z]+\\.[a-z]+$", + }) + expect(consoleWarnSpy).not.toHaveBeenCalled() + consoleWarnSpy.mockRestore() + }) + + it("should handle multiple invalid properties and warn for each", () => { + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => {}) + const descriptor = { + name: "field", + type: "string", + missingValues: "invalid", + categories: 123, + categoriesOrdered: "invalid", + jsonschema: "invalid", + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.missingValues).toBeUndefined() + expect(result.categories).toBeUndefined() + expect(result.categoriesOrdered).toBeUndefined() + expect(result.jsonschema).toBeUndefined() + expect(consoleWarnSpy).toHaveBeenCalledTimes(4) + consoleWarnSpy.mockRestore() + }) + + it("should handle field with all valid properties", () => { + const descriptor = { + name: "age", + type: "integer", + title: "Age", + description: "Person's age", + format: "fmt:default", + example: 25, + examples: [18, 25, 30, 45], + rdfType: "http://schema.org/age", + missingValues: ["", "unknown"], + categories: ["young", "middle", "old"], + categoriesOrdered: true, + jsonschema: { type: "integer", minimum: 0, maximum: 150 }, + constraints: { + required: true, + minimum: 0, + maximum: 150, + }, + } + + const result = convertFieldFromDescriptor(descriptor) + + expect(result.name).toBe("age") + expect(result.type).toBe("integer") + expect(result.title).toBe("Age") + expect(result.description).toBe("Person's age") + expect(result.format).toBe("default") + expect(result.example).toBe(25) + expect(result.examples).toEqual([18, 25, 30, 45]) + expect(result.rdfType).toBe("http://schema.org/age") + expect(result.missingValues).toEqual(["", "unknown"]) + expect(result.categories).toEqual(["young", "middle", "old"]) + expect(result.categoriesOrdered).toBe(true) + expect(result.jsonschema).toEqual({ + type: "integer", + minimum: 0, + maximum: 150, + }) + expect(result.constraints).toEqual({ + required: true, + minimum: 0, + maximum: 150, + }) + }) + }) +}) diff --git a/metadata/field/convert/toDescriptor.spec.ts b/metadata/field/convert/toDescriptor.spec.ts new file mode 100644 index 00000000..dcaaf349 --- /dev/null +++ b/metadata/field/convert/toDescriptor.spec.ts @@ -0,0 +1,354 @@ +import { describe, expect, it } from "vitest" +import type { Field } from "../Field.ts" +import { convertFieldToDescriptor } from "./toDescriptor.ts" + +describe("convertFieldToDescriptor", () => { + it("should return a cloned descriptor", () => { + const field: Field = { name: "id", type: "string" } + + const result = convertFieldToDescriptor(field) + + expect(result).toEqual(field) + expect(result).not.toBe(field) + }) + + it("should convert string field to descriptor", () => { + const field: Field = { + name: "email", + type: "string", + format: "email", + title: "Email Address", + description: "User's email address", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("email") + expect(result.type).toBe("string") + expect(result.format).toBe("email") + expect(result.title).toBe("Email Address") + expect(result.description).toBe("User's email address") + }) + + it("should convert integer field to descriptor", () => { + const field: Field = { + name: "age", + type: "integer", + title: "Age", + constraints: { + required: true, + minimum: 0, + maximum: 150, + }, + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("age") + expect(result.type).toBe("integer") + expect(result.title).toBe("Age") + expect(result.constraints).toEqual({ + required: true, + minimum: 0, + maximum: 150, + }) + }) + + it("should convert number field to descriptor", () => { + const field: Field = { + name: "score", + type: "number", + description: "Test score", + constraints: { + minimum: 0, + maximum: 100, + }, + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("score") + expect(result.type).toBe("number") + expect(result.description).toBe("Test score") + expect(result.constraints).toEqual({ + minimum: 0, + maximum: 100, + }) + }) + + it("should convert boolean field to descriptor", () => { + const field: Field = { + name: "active", + type: "boolean", + title: "Active Status", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("active") + expect(result.type).toBe("boolean") + expect(result.title).toBe("Active Status") + }) + + it("should convert date field to descriptor", () => { + const field: Field = { + name: "birth_date", + type: "date", + description: "Date of birth", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("birth_date") + expect(result.type).toBe("date") + expect(result.description).toBe("Date of birth") + }) + + it("should convert datetime field to descriptor", () => { + const field: Field = { + name: "created_at", + type: "datetime", + title: "Created At", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("created_at") + expect(result.type).toBe("datetime") + expect(result.title).toBe("Created At") + }) + + it("should convert time field to descriptor", () => { + const field: Field = { + name: "start_time", + type: "time", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("start_time") + expect(result.type).toBe("time") + }) + + it("should convert object field to descriptor", () => { + const field: Field = { + name: "metadata", + type: "object", + constraints: { + jsonSchema: { + type: "object", + properties: { + id: { type: "number" }, + name: { type: "string" }, + }, + }, + }, + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("metadata") + expect(result.type).toBe("object") + expect((result.constraints as any)?.jsonSchema).toEqual({ + type: "object", + properties: { + id: { type: "number" }, + name: { type: "string" }, + }, + }) + }) + + it("should convert array field to descriptor", () => { + const field: Field = { + name: "tags", + type: "array", + constraints: { + jsonSchema: { + type: "array", + items: { type: "string" }, + }, + }, + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("tags") + expect(result.type).toBe("array") + expect((result.constraints as any)?.jsonSchema).toEqual({ + type: "array", + items: { type: "string" }, + }) + }) + + it("should preserve string field with categories", () => { + const field: Field = { + name: "status", + type: "string", + categories: ["active", "inactive", "pending"], + categoriesOrdered: true, + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("status") + expect(result.type).toBe("string") + expect(result.categories).toEqual(["active", "inactive", "pending"]) + expect(result.categoriesOrdered).toBe(true) + }) + + it("should preserve field with missingValues", () => { + const field: Field = { + name: "value", + type: "string", + missingValues: ["", "NA", "N/A"], + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("value") + expect(result.type).toBe("string") + expect(result.missingValues).toEqual(["", "NA", "N/A"]) + }) + + it("should preserve field with example and examples", () => { + const field: Field = { + name: "age", + type: "integer", + example: 25, + examples: [18, 25, 30, 45], + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("age") + expect(result.type).toBe("integer") + expect(result.example).toBe(25) + expect(result.examples).toEqual([18, 25, 30, 45]) + }) + + it("should preserve field with rdfType", () => { + const field: Field = { + name: "age", + type: "integer", + rdfType: "http://schema.org/age", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("age") + expect(result.type).toBe("integer") + expect(result.rdfType).toBe("http://schema.org/age") + }) + + it("should convert field with all properties", () => { + const field: Field = { + name: "email", + type: "string", + format: "email", + title: "Email Address", + description: "User's email address", + example: "user@example.com", + examples: ["user@example.com", "admin@example.org"], + rdfType: "http://schema.org/email", + missingValues: ["", "none"], + categories: ["personal", "work"], + categoriesOrdered: false, + constraints: { + required: true, + pattern: "^[a-z]+@[a-z]+\\.[a-z]+$", + }, + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("email") + expect(result.type).toBe("string") + expect(result.format).toBe("email") + expect(result.title).toBe("Email Address") + expect(result.description).toBe("User's email address") + expect(result.example).toBe("user@example.com") + expect(result.examples).toEqual(["user@example.com", "admin@example.org"]) + expect(result.rdfType).toBe("http://schema.org/email") + expect(result.missingValues).toEqual(["", "none"]) + expect(result.categories).toEqual(["personal", "work"]) + expect(result.categoriesOrdered).toBe(false) + expect(result.constraints).toEqual({ + required: true, + pattern: "^[a-z]+@[a-z]+\\.[a-z]+$", + }) + }) + + it("should convert year field to descriptor", () => { + const field: Field = { + name: "birth_year", + type: "year", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("birth_year") + expect(result.type).toBe("year") + }) + + it("should convert yearmonth field to descriptor", () => { + const field: Field = { + name: "start_month", + type: "yearmonth", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("start_month") + expect(result.type).toBe("yearmonth") + }) + + it("should convert duration field to descriptor", () => { + const field: Field = { + name: "duration", + type: "duration", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("duration") + expect(result.type).toBe("duration") + }) + + it("should convert geopoint field to descriptor", () => { + const field: Field = { + name: "location", + type: "geopoint", + format: "default", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("location") + expect(result.type).toBe("geopoint") + expect(result.format).toBe("default") + }) + + it("should convert geojson field to descriptor", () => { + const field: Field = { + name: "geometry", + type: "geojson", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("geometry") + expect(result.type).toBe("geojson") + }) + + it("should convert any field to descriptor", () => { + const field: Field = { + name: "data", + type: "any", + } + + const result = convertFieldToDescriptor(field) + + expect(result.name).toBe("data") + expect(result.type).toBe("any") + }) +}) diff --git a/metadata/package/save.spec.ts b/metadata/package/save.spec.ts new file mode 100644 index 00000000..e052760b --- /dev/null +++ b/metadata/package/save.spec.ts @@ -0,0 +1,372 @@ +import * as fs from "node:fs/promises" +import * as path from "node:path" +import { temporaryDirectory } from "tempy" +import { afterEach, beforeEach, describe, expect, it } from "vitest" +import type { Package } from "./Package.ts" +import { savePackageDescriptor } from "./save.ts" + +describe("savePackageDescriptor", () => { + let testDir: string + let testPath: string + let testPackage: Package + + beforeEach(() => { + testDir = temporaryDirectory() + testPath = path.join(testDir, "datapackage.json") + testPackage = { + name: "test-package", + resources: [ + { + name: "test-resource", + path: path.join(testDir, "data.csv"), + }, + ], + } + }) + + afterEach(async () => { + try { + await fs.rm(testDir, { recursive: true, force: true }) + } catch (error) { + if (error instanceof Error && !error.message.includes("ENOENT")) { + console.error(`Failed to clean up test directory: ${testDir}`, error) + } + } + }) + + it("should save a package descriptor to a file and maintain its structure", async () => { + await savePackageDescriptor(testPackage, { path: testPath }) + + const fileExists = await fs + .stat(testPath) + .then(() => true) + .catch(() => false) + expect(fileExists).toBe(true) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + + const { $schema, ...packageWithoutSchema } = parsedContent + expect(packageWithoutSchema.name).toEqual(testPackage.name) + expect(packageWithoutSchema.resources).toHaveLength(1) + expect(packageWithoutSchema.resources[0].name).toBe("test-resource") + expect(packageWithoutSchema.resources[0].path).toBe("data.csv") + expect($schema).toBe( + "https://datapackage.org/profiles/2.0/datapackage.json", + ) + }) + + it("should add $schema property if not present", async () => { + await savePackageDescriptor(testPackage, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.$schema).toBe( + "https://datapackage.org/profiles/2.0/datapackage.json", + ) + }) + + it("should preserve existing $schema property", async () => { + const packageWithSchema: Package = { + name: "test-package", + resources: [ + { + name: "test-resource", + path: path.join(testDir, "data.csv"), + }, + ], + $schema: "https://custom.schema.url", + } + + await savePackageDescriptor(packageWithSchema, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.$schema).toBe("https://custom.schema.url") + }) + + it("should use pretty formatting with 2-space indentation", async () => { + await savePackageDescriptor(testPackage, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const lines = content.split("\n") + expect(lines.length).toBeGreaterThan(1) + + if (lines.length > 1 && lines[1]) { + expect(lines[1].startsWith(" ")).toBe(true) + } + }) + + it("should save package with multiple resources", async () => { + const packageWithMultipleResources: Package = { + name: "test-package", + resources: [ + { + name: "resource1", + path: path.join(testDir, "data1.csv"), + }, + { + name: "resource2", + path: path.join(testDir, "data2.json"), + format: "json", + }, + ], + } + + await savePackageDescriptor(packageWithMultipleResources, { + path: testPath, + }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.resources).toHaveLength(2) + expect(parsedContent.resources[0]?.name).toBe("resource1") + expect(parsedContent.resources[1]?.name).toBe("resource2") + }) + + it("should save package with resource containing schema", async () => { + const packageWithSchema: Package = { + name: "test-package", + resources: [ + { + name: "test-resource", + path: path.join(testDir, "data.csv"), + schema: { + fields: [ + { name: "id", type: "integer" }, + { name: "name", type: "string" }, + ], + }, + }, + ], + } + + await savePackageDescriptor(packageWithSchema, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.resources[0]?.schema).toEqual( + packageWithSchema.resources[0]?.schema, + ) + }) + + it("should save package with resource containing dialect", async () => { + const packageWithDialect: Package = { + name: "test-package", + resources: [ + { + name: "test-resource", + path: path.join(testDir, "data.csv"), + dialect: { + delimiter: ";", + lineTerminator: "\n", + }, + }, + ], + } + + await savePackageDescriptor(packageWithDialect, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.resources[0]?.dialect).toEqual( + packageWithDialect.resources[0]?.dialect, + ) + }) + + it("should save package to a nested directory path", async () => { + const nestedPath = path.join(testDir, "nested", "dir", "datapackage.json") + const nestedPackage: Package = { + name: "test-package", + resources: [ + { + name: "test-resource", + path: path.join(testDir, "nested", "dir", "data.csv"), + }, + ], + } + + await savePackageDescriptor(nestedPackage, { path: nestedPath }) + + const fileExists = await fs + .stat(nestedPath) + .then(() => true) + .catch(() => false) + expect(fileExists).toBe(true) + + const content = await fs.readFile(nestedPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.name).toBe(nestedPackage.name) + }) + + it("should throw an error when file exists and overwrite is false", async () => { + await savePackageDescriptor(testPackage, { path: testPath }) + + await expect( + savePackageDescriptor(testPackage, { + path: testPath, + overwrite: false, + }), + ).rejects.toThrow() + }) + + it("should throw an error when file exists and overwrite is not specified", async () => { + await savePackageDescriptor(testPackage, { path: testPath }) + + await expect( + savePackageDescriptor(testPackage, { path: testPath }), + ).rejects.toThrow() + }) + + it("should overwrite existing file when overwrite is true", async () => { + const initialPackage: Package = { + name: "initial", + resources: [ + { + name: "resource1", + path: path.join(testDir, "data1.csv"), + }, + ], + } + + const updatedPackage: Package = { + name: "updated", + resources: [ + { + name: "resource2", + path: path.join(testDir, "data2.csv"), + }, + ], + description: "Updated package", + } + + await savePackageDescriptor(initialPackage, { path: testPath }) + + const initialContent = await fs.readFile(testPath, "utf-8") + const initialParsed = JSON.parse(initialContent) + expect(initialParsed.name).toBe("initial") + + await savePackageDescriptor(updatedPackage, { + path: testPath, + overwrite: true, + }) + + const updatedContent = await fs.readFile(testPath, "utf-8") + const updatedParsed = JSON.parse(updatedContent) + expect(updatedParsed.name).toBe("updated") + expect(updatedParsed.description).toBe("Updated package") + }) + + it("should save package with all metadata fields", async () => { + const fullPackage: Package = { + name: "full-package", + title: "Full Package", + description: "A package with all fields", + version: "1.0.0", + homepage: "https://example.com", + keywords: ["test", "data", "package"], + created: "2024-01-01T00:00:00Z", + image: "https://example.com/image.png", + resources: [ + { + name: "test-resource", + path: path.join(testDir, "data.csv"), + }, + ], + } + + await savePackageDescriptor(fullPackage, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.name).toBe(fullPackage.name) + expect(parsedContent.title).toBe(fullPackage.title) + expect(parsedContent.description).toBe(fullPackage.description) + expect(parsedContent.version).toBe(fullPackage.version) + expect(parsedContent.homepage).toBe(fullPackage.homepage) + expect(parsedContent.keywords).toEqual(fullPackage.keywords) + expect(parsedContent.created).toBe(fullPackage.created) + expect(parsedContent.image).toBe(fullPackage.image) + }) + + it("should save package with contributors", async () => { + const packageWithContributors: Package = { + name: "test-package", + resources: [ + { + name: "test-resource", + path: path.join(testDir, "data.csv"), + }, + ], + contributors: [ + { + title: "John Doe", + email: "john@example.com", + role: "author", + }, + { + title: "Jane Smith", + path: "https://example.org", + }, + ], + } + + await savePackageDescriptor(packageWithContributors, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.contributors).toHaveLength(2) + expect(parsedContent.contributors[0]?.title).toBe("John Doe") + expect(parsedContent.contributors[1]?.title).toBe("Jane Smith") + }) + + it("should save package with licenses", async () => { + const packageWithLicenses: Package = { + name: "test-package", + resources: [ + { + name: "test-resource", + path: path.join(testDir, "data.csv"), + }, + ], + licenses: [ + { + name: "MIT", + path: "https://opensource.org/licenses/MIT", + }, + ], + } + + await savePackageDescriptor(packageWithLicenses, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.licenses).toHaveLength(1) + expect(parsedContent.licenses[0]?.name).toBe("MIT") + }) + + it("should save package with sources", async () => { + const packageWithSources: Package = { + name: "test-package", + resources: [ + { + name: "test-resource", + path: path.join(testDir, "data.csv"), + }, + ], + sources: [ + { + title: "Example Source", + path: "https://example.com/data", + }, + ], + } + + await savePackageDescriptor(packageWithSources, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.sources).toHaveLength(1) + expect(parsedContent.sources[0]?.title).toBe("Example Source") + }) +}) diff --git a/metadata/resource/save.spec.ts b/metadata/resource/save.spec.ts new file mode 100644 index 00000000..dce27a2c --- /dev/null +++ b/metadata/resource/save.spec.ts @@ -0,0 +1,215 @@ +import * as fs from "node:fs/promises" +import * as path from "node:path" +import { temporaryDirectory } from "tempy" +import { afterEach, beforeEach, describe, expect, it } from "vitest" +import type { Resource } from "./Resource.ts" +import { saveResourceDescriptor } from "./save.ts" + +describe("saveResourceDescriptor", () => { + let testDir: string + let testPath: string + let testResource: Resource + + beforeEach(() => { + testDir = temporaryDirectory() + testPath = path.join(testDir, "resource.json") + testResource = { + name: "test-resource", + path: path.join(testDir, "data.csv"), + } + }) + + afterEach(async () => { + try { + await fs.rm(testDir, { recursive: true, force: true }) + } catch (error) { + if (error instanceof Error && !error.message.includes("ENOENT")) { + console.error(`Failed to clean up test directory: ${testDir}`, error) + } + } + }) + + it("should save a resource descriptor to a file and maintain its structure", async () => { + await saveResourceDescriptor(testResource, { path: testPath }) + + const fileExists = await fs + .stat(testPath) + .then(() => true) + .catch(() => false) + expect(fileExists).toBe(true) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + + const { $schema, ...resourceWithoutSchema } = parsedContent + expect(resourceWithoutSchema.name).toEqual(testResource.name) + expect(resourceWithoutSchema.path).toBe("data.csv") + expect($schema).toBe( + "https://datapackage.org/profiles/2.0/dataresource.json", + ) + }) + + it("should add $schema property if not present", async () => { + await saveResourceDescriptor(testResource, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.$schema).toBe( + "https://datapackage.org/profiles/2.0/dataresource.json", + ) + }) + + it("should preserve existing $schema property", async () => { + const resourceWithSchema: Resource = { + name: "test-resource", + path: path.join(testDir, "data.csv"), + $schema: "https://custom.schema.url", + } + + await saveResourceDescriptor(resourceWithSchema, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.$schema).toBe("https://custom.schema.url") + }) + + it("should use pretty formatting with 2-space indentation", async () => { + await saveResourceDescriptor(testResource, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const lines = content.split("\n") + expect(lines.length).toBeGreaterThan(1) + + if (lines.length > 1 && lines[1]) { + expect(lines[1].startsWith(" ")).toBe(true) + } + }) + + it("should save resource with schema", async () => { + const resourceWithSchema: Resource = { + name: "test-resource", + path: path.join(testDir, "data.csv"), + schema: { + fields: [ + { name: "id", type: "integer" }, + { name: "name", type: "string" }, + ], + }, + } + + await saveResourceDescriptor(resourceWithSchema, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.schema).toEqual(resourceWithSchema.schema) + }) + + it("should save resource with dialect", async () => { + const resourceWithDialect: Resource = { + name: "test-resource", + path: path.join(testDir, "data.csv"), + dialect: { + delimiter: ";", + lineTerminator: "\n", + }, + } + + await saveResourceDescriptor(resourceWithDialect, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.dialect).toEqual(resourceWithDialect.dialect) + }) + + it("should save resource to a nested directory path", async () => { + const nestedPath = path.join(testDir, "nested", "dir", "resource.json") + const nestedResource: Resource = { + name: "test-resource", + path: path.join(testDir, "nested", "dir", "data.csv"), + } + + await saveResourceDescriptor(nestedResource, { path: nestedPath }) + + const fileExists = await fs + .stat(nestedPath) + .then(() => true) + .catch(() => false) + expect(fileExists).toBe(true) + + const content = await fs.readFile(nestedPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.name).toBe(nestedResource.name) + }) + + it("should throw an error when file exists and overwrite is false", async () => { + await saveResourceDescriptor(testResource, { path: testPath }) + + await expect( + saveResourceDescriptor(testResource, { + path: testPath, + overwrite: false, + }), + ).rejects.toThrow() + }) + + it("should throw an error when file exists and overwrite is not specified", async () => { + await saveResourceDescriptor(testResource, { path: testPath }) + + await expect( + saveResourceDescriptor(testResource, { path: testPath }), + ).rejects.toThrow() + }) + + it("should overwrite existing file when overwrite is true", async () => { + const initialResource: Resource = { + name: "initial", + path: path.join(testDir, "data1.csv"), + } + + const updatedResource: Resource = { + name: "updated", + path: path.join(testDir, "data2.csv"), + description: "Updated resource", + } + + await saveResourceDescriptor(initialResource, { path: testPath }) + + const initialContent = await fs.readFile(testPath, "utf-8") + const initialParsed = JSON.parse(initialContent) + expect(initialParsed.name).toBe("initial") + + await saveResourceDescriptor(updatedResource, { + path: testPath, + overwrite: true, + }) + + const updatedContent = await fs.readFile(testPath, "utf-8") + const updatedParsed = JSON.parse(updatedContent) + expect(updatedParsed.name).toBe("updated") + expect(updatedParsed.description).toBe("Updated resource") + }) + + it("should save resource with all metadata fields", async () => { + const fullResource: Resource = { + name: "full-resource", + path: path.join(testDir, "data.csv"), + title: "Full Resource", + description: "A resource with all fields", + format: "csv", + mediatype: "text/csv", + encoding: "utf-8", + bytes: 1024, + hash: "abc123", + } + + await saveResourceDescriptor(fullResource, { path: testPath }) + + const content = await fs.readFile(testPath, "utf-8") + const parsedContent = JSON.parse(content) + expect(parsedContent.name).toBe(fullResource.name) + expect(parsedContent.title).toBe(fullResource.title) + expect(parsedContent.description).toBe(fullResource.description) + expect(parsedContent.format).toBe(fullResource.format) + expect(parsedContent.bytes).toBe(fullResource.bytes) + }) +}) diff --git a/metadata/schema/fixtures/schema-full.json b/metadata/schema/fixtures/schema-full.json new file mode 100644 index 00000000..05dc40d7 --- /dev/null +++ b/metadata/schema/fixtures/schema-full.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://datapackage.org/profiles/2.0/tableschema.json", + "name": "users", + "title": "User Data Schema", + "description": "A comprehensive schema for user data", + "fields": [ + { + "name": "id", + "type": "integer", + "constraints": { + "required": true, + "unique": true + } + }, + { + "name": "email", + "type": "string", + "format": "email", + "constraints": { + "required": true + } + }, + { + "name": "name", + "type": "string", + "constraints": { + "minLength": 1, + "maxLength": 100 + } + }, + { + "name": "age", + "type": "integer", + "constraints": { + "minimum": 0, + "maximum": 150 + } + }, + { + "name": "score", + "type": "number" + }, + { + "name": "active", + "type": "boolean" + }, + { + "name": "created_at", + "type": "datetime" + }, + { + "name": "birth_date", + "type": "date" + }, + { + "name": "department_id", + "type": "integer" + } + ], + "primaryKey": "id", + "uniqueKeys": [["email"], ["name", "department_id"]], + "foreignKeys": [ + { + "fields": "department_id", + "reference": { + "resource": "departments", + "fields": "id" + } + } + ], + "missingValues": ["", "NA", "N/A", "null"] +} diff --git a/metadata/schema/load.spec.ts b/metadata/schema/load.spec.ts index fef702ef..1a9e1374 100644 --- a/metadata/schema/load.spec.ts +++ b/metadata/schema/load.spec.ts @@ -31,4 +31,43 @@ describe("loadSchema", () => { loadSchema(getFixturePath("schema-invalid.json")), ).rejects.toThrow() }) + + it("loads a full schema with all features", async () => { + const schema = await loadSchema(getFixturePath("schema-full.json")) + + expectTypeOf(schema).toEqualTypeOf() + expect(schema).toBeDefined() + expect(schema.$schema).toBe( + "https://datapackage.org/profiles/2.0/tableschema.json", + ) + expect(schema.name).toBe("users") + expect(schema.title).toBe("User Data Schema") + expect(schema.description).toBe("A comprehensive schema for user data") + + expect(schema.fields).toHaveLength(9) + expect(schema.fields[0]?.name).toBe("id") + expect(schema.fields[0]?.type).toBe("integer") + expect(schema.fields[0]?.constraints?.required).toBe(true) + expect(schema.fields[0]?.constraints?.unique).toBe(true) + + expect(schema.fields[1]?.name).toBe("email") + expect(schema.fields[1]?.type).toBe("string") + expect(schema.fields[1]?.format).toBe("email") + + expect(schema.fields[6]?.name).toBe("created_at") + expect(schema.fields[6]?.type).toBe("datetime") + + expect(schema.primaryKey).toEqual(["id"]) + + expect(schema.uniqueKeys).toHaveLength(2) + expect(schema.uniqueKeys?.[0]).toEqual(["email"]) + expect(schema.uniqueKeys?.[1]).toEqual(["name", "department_id"]) + + expect(schema.foreignKeys).toHaveLength(1) + expect(schema.foreignKeys?.[0]?.fields).toEqual(["department_id"]) + expect(schema.foreignKeys?.[0]?.reference?.resource).toBe("departments") + expect(schema.foreignKeys?.[0]?.reference?.fields).toEqual(["id"]) + + expect(schema.missingValues).toEqual(["", "NA", "N/A", "null"]) + }) }) diff --git a/terminal/commands/dialect/explore.spec.tsx b/terminal/commands/dialect/explore.spec.tsx index 61a3c0a0..3a7231fe 100644 --- a/terminal/commands/dialect/explore.spec.tsx +++ b/terminal/commands/dialect/explore.spec.tsx @@ -4,8 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest" import * as sessionModule from "../../session.ts" import { exploreDialectCommand } from "./explore.tsx" -vi.mock("../../components/DialectGrid.tsx", () => ({ - DialectGrid: vi.fn(() => null), +vi.mock("../../components/Dialect/Dialect.tsx", () => ({ + Dialect: vi.fn(() => null), })) describe("dialect explore", () => { diff --git a/terminal/commands/dialect/explore.tsx b/terminal/commands/dialect/explore.tsx index 3ed7ac77..c9725956 100644 --- a/terminal/commands/dialect/explore.tsx +++ b/terminal/commands/dialect/explore.tsx @@ -3,7 +3,7 @@ import type { Resource } from "@dpkit/library" import { resolveDialect } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { DialectGrid } from "../../components/DialectGrid.tsx" +import { Dialect } from "../../components/Dialect/index.ts" import { helpConfiguration } from "../../helpers/help.ts" import { isEmptyObject } from "../../helpers/object.ts" import { selectResource } from "../../helpers/resource.ts" @@ -41,5 +41,5 @@ export const exploreDialectCommand = new Command("explore") process.exit(1) // typescript ignore never return type above } - await session.render(dialect, ) + await session.render(dialect, ) }) diff --git a/terminal/commands/dialect/infer.tsx b/terminal/commands/dialect/infer.tsx index 0485e846..ef2f1f91 100644 --- a/terminal/commands/dialect/infer.tsx +++ b/terminal/commands/dialect/infer.tsx @@ -2,7 +2,7 @@ import { inferDialect } from "@dpkit/library" import type { Resource } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { DialectGrid } from "../../components/DialectGrid.tsx" +import { Dialect } from "../../components/Dialect/index.ts" import { helpConfiguration } from "../../helpers/help.ts" import { isEmptyObject } from "../../helpers/object.ts" import { selectResource } from "../../helpers/resource.ts" @@ -41,5 +41,5 @@ export const inferDialectCommand = new Command("infer") process.exit(1) // typescript ignore never return type above } - await session.render(dialect, ) + await session.render(dialect, ) }) diff --git a/terminal/commands/dialect/validate.tsx b/terminal/commands/dialect/validate.tsx index 5dd4224c..b5a7d5d8 100644 --- a/terminal/commands/dialect/validate.tsx +++ b/terminal/commands/dialect/validate.tsx @@ -3,7 +3,7 @@ import type { Resource } from "@dpkit/library" import { resolveDialect } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { ErrorGrid } from "../../components/ErrorGrid.tsx" +import { Report } from "../../components/Report/index.ts" import { selectErrorType } from "../../helpers/error.ts" import { helpConfiguration } from "../../helpers/help.ts" import { selectResource } from "../../helpers/resource.ts" @@ -67,6 +67,6 @@ export const validateDialectCommand = new Command("validate") session.render( report, - , + , ) }) diff --git a/terminal/commands/file/describe.tsx b/terminal/commands/file/describe.tsx index 7014b8a4..0e93bc0a 100644 --- a/terminal/commands/file/describe.tsx +++ b/terminal/commands/file/describe.tsx @@ -1,7 +1,7 @@ import { describeFile } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { DataGrid } from "../../components/DataGrid.tsx" +import { Datagrid } from "../../components/Datagrid/index.ts" import { helpConfiguration } from "../../helpers/help.ts" import { selectResource } from "../../helpers/resource.ts" import * as params from "../../params/index.ts" @@ -41,5 +41,5 @@ export const describeFileCommand = new Command("describe") describeFile(path, { hashType: options.hashType }), ) - session.render(stats, ) + session.render(stats, ) }) diff --git a/terminal/commands/file/validate.tsx b/terminal/commands/file/validate.tsx index e28cc073..5652d500 100644 --- a/terminal/commands/file/validate.tsx +++ b/terminal/commands/file/validate.tsx @@ -1,7 +1,7 @@ import { validateFile } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { ErrorGrid } from "../../components/ErrorGrid.tsx" +import { Report } from "../../components/Report/index.ts" import { selectErrorType } from "../../helpers/error.ts" import { helpConfiguration } from "../../helpers/help.ts" import * as params from "../../params/index.ts" @@ -54,6 +54,6 @@ export const validateFileCommand = new Command("validate") session.render( report, - , + , ) }) diff --git a/terminal/commands/package/explore.spec.tsx b/terminal/commands/package/explore.spec.tsx index be946a3f..74b90f46 100644 --- a/terminal/commands/package/explore.spec.tsx +++ b/terminal/commands/package/explore.spec.tsx @@ -4,8 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest" import * as sessionModule from "../../session.ts" import { explorePackageCommand } from "./explore.tsx" -vi.mock("../../components/PackageGrid.tsx", () => ({ - PackageGrid: vi.fn(() => null), +vi.mock("../../components/Package/Package.tsx", () => ({ + Package: vi.fn(() => null), })) describe("package explore", () => { diff --git a/terminal/commands/package/explore.tsx b/terminal/commands/package/explore.tsx index f1aee371..aa4af147 100644 --- a/terminal/commands/package/explore.tsx +++ b/terminal/commands/package/explore.tsx @@ -1,7 +1,7 @@ import { loadPackage } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { PackageGrid } from "../../components/PackageGrid.tsx" +import { Package } from "../../components/Package/index.ts" import { helpConfiguration } from "../../helpers/help.ts" import * as params from "../../params/index.ts" import { Session } from "../../session.ts" @@ -23,5 +23,5 @@ export const explorePackageCommand = new Command("explore") const dataPackage = await session.task("Loading package", loadPackage(path)) - await session.render(dataPackage, ) + await session.render(dataPackage, ) }) diff --git a/terminal/commands/package/infer.tsx b/terminal/commands/package/infer.tsx index ef98cb4a..0e5650a6 100644 --- a/terminal/commands/package/infer.tsx +++ b/terminal/commands/package/infer.tsx @@ -1,7 +1,7 @@ import { inferPackage } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { PackageGrid } from "../../components/PackageGrid.tsx" +import { Package } from "../../components/Package/index.ts" import { helpConfiguration } from "../../helpers/help.ts" import * as params from "../../params/index.ts" import { Session } from "../../session.ts" @@ -74,8 +74,5 @@ export const inferPackageCommand = new Command("infer") inferPackage(sourcePackage, options), ) - await session.render( - targetPackage, - , - ) + await session.render(targetPackage, ) }) diff --git a/terminal/commands/package/validate.tsx b/terminal/commands/package/validate.tsx index ef3073bc..64eb58a0 100644 --- a/terminal/commands/package/validate.tsx +++ b/terminal/commands/package/validate.tsx @@ -1,7 +1,7 @@ import { validatePackage } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { ErrorGrid } from "../../components/ErrorGrid.tsx" +import { Report } from "../../components/Report/index.ts" import { selectErrorResource, selectErrorType } from "../../helpers/error.ts" import { helpConfiguration } from "../../helpers/help.ts" import * as params from "../../params/index.ts" @@ -51,6 +51,6 @@ export const validatePackageCommand = new Command("validate") session.render( report, // @ts-ignore - , + , ) }) diff --git a/terminal/commands/resource/explore.spec.tsx b/terminal/commands/resource/explore.spec.tsx index 851fb685..efa6f824 100644 --- a/terminal/commands/resource/explore.spec.tsx +++ b/terminal/commands/resource/explore.spec.tsx @@ -4,8 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest" import * as sessionModule from "../../session.ts" import { exploreResourceCommand } from "./explore.tsx" -vi.mock("../../components/ResourceGrid.tsx", () => ({ - ResourceGrid: vi.fn(() => null), +vi.mock("../../components/Resource/Resource.tsx", () => ({ + Resource: vi.fn(() => null), })) describe("resource explore", () => { diff --git a/terminal/commands/resource/explore.tsx b/terminal/commands/resource/explore.tsx index 99680507..ec54f9ff 100644 --- a/terminal/commands/resource/explore.tsx +++ b/terminal/commands/resource/explore.tsx @@ -1,7 +1,7 @@ import { loadResourceDescriptor } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { ResourceGrid } from "../../components/ResourceGrid.tsx" +import { Resource } from "../../components/Resource/index.ts" import { helpConfiguration } from "../../helpers/help.ts" import { isEmptyObject } from "../../helpers/object.ts" import { selectResource } from "../../helpers/resource.ts" @@ -33,5 +33,5 @@ export const exploreResourceCommand = new Command("explore") process.exit(1) // typescript ignore never return type above } - await session.render(resource, ) + await session.render(resource, ) }) diff --git a/terminal/commands/resource/infer.tsx b/terminal/commands/resource/infer.tsx index 21eaf460..a1b1a733 100644 --- a/terminal/commands/resource/infer.tsx +++ b/terminal/commands/resource/infer.tsx @@ -1,7 +1,7 @@ import { inferResource } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { ResourceGrid } from "../../components/ResourceGrid.tsx" +import { Resource } from "../../components/Resource/index.ts" import { helpConfiguration } from "../../helpers/help.ts" import { isEmptyObject } from "../../helpers/object.ts" import { selectResource } from "../../helpers/resource.ts" @@ -81,5 +81,5 @@ export const inferResourceCommand = new Command("infer") process.exit(1) // typescript ignore never return type above } - await session.render(result, ) + await session.render(result, ) }) diff --git a/terminal/commands/resource/validate.tsx b/terminal/commands/resource/validate.tsx index ca437a69..787b549a 100644 --- a/terminal/commands/resource/validate.tsx +++ b/terminal/commands/resource/validate.tsx @@ -1,7 +1,7 @@ import { validateResource } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { ErrorGrid } from "../../components/ErrorGrid.tsx" +import { Report } from "../../components/Report/index.ts" import { selectErrorType } from "../../helpers/error.ts" import { helpConfiguration } from "../../helpers/help.ts" import { selectResource } from "../../helpers/resource.ts" @@ -53,6 +53,6 @@ export const validateResourceCommand = new Command("validate") session.render( report, - , + , ) }) diff --git a/terminal/commands/schema/explore.spec.tsx b/terminal/commands/schema/explore.spec.tsx index 4d97ad40..c63906ac 100644 --- a/terminal/commands/schema/explore.spec.tsx +++ b/terminal/commands/schema/explore.spec.tsx @@ -4,8 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest" import * as sessionModule from "../../session.ts" import { exploreSchemaCommand } from "./explore.tsx" -vi.mock("../../components/SchemaGrid.tsx", () => ({ - SchemaGrid: vi.fn(() => null), +vi.mock("../../components/Schema/Schema.tsx", () => ({ + Schema: vi.fn(() => null), })) describe("schema explore", () => { diff --git a/terminal/commands/schema/explore.tsx b/terminal/commands/schema/explore.tsx index 92cbcf02..665d6394 100644 --- a/terminal/commands/schema/explore.tsx +++ b/terminal/commands/schema/explore.tsx @@ -3,7 +3,7 @@ import type { Resource } from "@dpkit/library" import { resolveSchema } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { SchemaGrid } from "../../components/SchemaGrid.tsx" +import { Schema } from "../../components/Schema/index.ts" import { helpConfiguration } from "../../helpers/help.ts" import { isEmptyObject } from "../../helpers/object.ts" import { selectResource } from "../../helpers/resource.ts" @@ -41,5 +41,5 @@ export const exploreSchemaCommand = new Command("explore") process.exit(1) // typescript ignore never return type above } - await session.render(schema, ) + await session.render(schema, ) }) diff --git a/terminal/commands/schema/infer.tsx b/terminal/commands/schema/infer.tsx index e0497e27..e645b692 100644 --- a/terminal/commands/schema/infer.tsx +++ b/terminal/commands/schema/infer.tsx @@ -1,7 +1,7 @@ import { inferSchemaFromTable, loadTable } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { SchemaGrid } from "../../components/SchemaGrid.tsx" +import { Schema } from "../../components/Schema/index.ts" import { createDialectFromOptions } from "../../helpers/dialect.ts" import { helpConfiguration } from "../../helpers/help.ts" import { isEmptyObject } from "../../helpers/object.ts" @@ -94,5 +94,5 @@ export const inferSchemaCommand = new Command("infer") process.exit(1) } - await session.render(inferredSchema, ) + await session.render(inferredSchema, ) }) diff --git a/terminal/commands/schema/validate.tsx b/terminal/commands/schema/validate.tsx index 0efe87b1..2f17bfdd 100644 --- a/terminal/commands/schema/validate.tsx +++ b/terminal/commands/schema/validate.tsx @@ -3,7 +3,7 @@ import { resolveSchema } from "@dpkit/library" import type { Resource } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { ErrorGrid } from "../../components/ErrorGrid.tsx" +import { Report } from "../../components/Report/index.ts" import { selectErrorType } from "../../helpers/error.ts" import { helpConfiguration } from "../../helpers/help.ts" import { selectResource } from "../../helpers/resource.ts" @@ -67,6 +67,6 @@ export const validateSchemaCommand = new Command("validate") session.render( report, - , + , ) }) diff --git a/terminal/commands/table/describe.tsx b/terminal/commands/table/describe.tsx index 53d508d0..6614182f 100644 --- a/terminal/commands/table/describe.tsx +++ b/terminal/commands/table/describe.tsx @@ -5,7 +5,7 @@ import type { Resource } from "@dpkit/library" import { loadDialect } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { DataGrid } from "../../components/DataGrid.tsx" +import { Datagrid } from "../../components/Datagrid/index.ts" import { createDialectFromOptions } from "../../helpers/dialect.ts" import { helpConfiguration } from "../../helpers/help.ts" import { selectResource } from "../../helpers/resource.ts" @@ -107,5 +107,5 @@ export const describeTableCommand = new Command("describe") const stats = frame.describe().rename({ describe: "#" }) const records = stats.toRecords() - session.render(records, ) + session.render(records, ) }) diff --git a/terminal/commands/table/explore.spec.tsx b/terminal/commands/table/explore.spec.tsx index e07d11d1..5b35d332 100644 --- a/terminal/commands/table/explore.spec.tsx +++ b/terminal/commands/table/explore.spec.tsx @@ -4,8 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest" import * as sessionModule from "../../session.ts" import { exploreTableCommand } from "./explore.tsx" -vi.mock("../../components/TableGrid.tsx", () => ({ - TableGrid: vi.fn(() => null), +vi.mock("../../components/Table/Table.tsx", () => ({ + Table: vi.fn(() => null), })) describe("table explore", () => { diff --git a/terminal/commands/table/explore.tsx b/terminal/commands/table/explore.tsx index 18586fc1..0d0460a2 100644 --- a/terminal/commands/table/explore.tsx +++ b/terminal/commands/table/explore.tsx @@ -5,7 +5,7 @@ import { loadDialect, loadTable, normalizeTable } from "@dpkit/library" import type { Resource } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { TableGrid } from "../../components/TableGrid.tsx" +import { Table } from "../../components/Table/index.ts" import { createDialectFromOptions } from "../../helpers/dialect.ts" import { helpConfiguration } from "../../helpers/help.ts" import { selectResource } from "../../helpers/resource.ts" @@ -123,6 +123,6 @@ export const exploreTableCommand = new Command("explore") await session.render( table, - , + , ) }) diff --git a/terminal/commands/table/validate.tsx b/terminal/commands/table/validate.tsx index 3490b922..969f0a04 100644 --- a/terminal/commands/table/validate.tsx +++ b/terminal/commands/table/validate.tsx @@ -6,7 +6,7 @@ import { loadDialect } from "@dpkit/library" import type { Resource } from "@dpkit/library" import { Command } from "commander" import React from "react" -import { ErrorGrid } from "../../components/ErrorGrid.tsx" +import { Report } from "../../components/Report/index.ts" import { createDialectFromOptions } from "../../helpers/dialect.ts" import { selectErrorType } from "../../helpers/error.ts" import { helpConfiguration } from "../../helpers/help.ts" @@ -136,6 +136,6 @@ export const validateTableCommand = new Command("validate") session.render( createReport(errors), - , + , ) }) diff --git a/terminal/components/DataGrid.spec.tsx b/terminal/components/Datagrid/Datagrid.spec.tsx similarity index 81% rename from terminal/components/DataGrid.spec.tsx rename to terminal/components/Datagrid/Datagrid.spec.tsx index 7ee0b86b..0147ddcd 100644 --- a/terminal/components/DataGrid.spec.tsx +++ b/terminal/components/Datagrid/Datagrid.spec.tsx @@ -2,12 +2,12 @@ import type { DataRecord } from "@dpkit/library" import { render } from "ink-testing-library" import React from "react" import { describe, expect, it } from "vitest" -import { DataGrid } from "./DataGrid.tsx" +import { Datagrid } from "./Datagrid.tsx" -describe("DataGrid", () => { +describe("Datagrid", () => { it("should render empty grid with no records", () => { const records: DataRecord[] = [] - const { lastFrame } = render() + const { lastFrame } = render() expect(lastFrame()).toBeDefined() }) @@ -17,7 +17,7 @@ describe("DataGrid", () => { { id: 1, name: "alice" }, { id: 2, name: "bob" }, ] - const { lastFrame } = render() + const { lastFrame } = render() const output = lastFrame() expect(output).toContain("id") @@ -37,7 +37,7 @@ describe("DataGrid", () => { { name: "value", type: "number" as const }, ], } - const { lastFrame } = render() + const { lastFrame } = render() const output = lastFrame() expect(output).toContain("id") @@ -46,7 +46,7 @@ describe("DataGrid", () => { it("should render with types when withTypes is true", () => { const records: DataRecord[] = [{ id: 1, name: "alice" }] - const { lastFrame } = render() + const { lastFrame } = render() const output = lastFrame() expect(output).toBeDefined() @@ -59,7 +59,7 @@ describe("DataGrid", () => { { id: 1, name: "alice" }, { id: 2, name: "bob" }, ] - const { lastFrame } = render() + const { lastFrame } = render() const output = lastFrame() expect(output).toContain("id") @@ -71,7 +71,7 @@ describe("DataGrid", () => { { id: 1, name: "alice" }, { id: 2, name: "bob" }, ] - const { lastFrame } = render() + const { lastFrame } = render() const output = lastFrame() expect(output).toContain("alice") @@ -83,7 +83,7 @@ describe("DataGrid", () => { { id: 1, name: "alice" }, { id: 2, name: "bob" }, ] - const { lastFrame } = render() + const { lastFrame } = render() const output = lastFrame() expect(output).toContain("alice") @@ -96,7 +96,7 @@ describe("DataGrid", () => { { id: 2, name: "bob" }, ] const { lastFrame } = render( - , + , ) const output = lastFrame() @@ -109,7 +109,7 @@ describe("DataGrid", () => { { id: 2, name: "bob" }, ] const { lastFrame } = render( - , + , ) const output = lastFrame() @@ -118,7 +118,7 @@ describe("DataGrid", () => { it("should render with green border by default", () => { const records: DataRecord[] = [{ id: 1, name: "alice" }] - const { lastFrame } = render() + const { lastFrame } = render() expect(lastFrame()).toBeDefined() }) @@ -126,7 +126,7 @@ describe("DataGrid", () => { it("should render with red border when specified", () => { const records: DataRecord[] = [{ id: 1, name: "alice" }] const { lastFrame } = render( - , + , ) expect(lastFrame()).toBeDefined() @@ -138,7 +138,7 @@ describe("DataGrid", () => { { id: 2, name: "bob", age: 25 }, { id: 3, name: "charlie", age: 35 }, ] - const { lastFrame } = render() + const { lastFrame } = render() const output = lastFrame() expect(output).toContain("alice") @@ -154,7 +154,7 @@ describe("DataGrid", () => { { id: 1, value: 100.5 }, { id: 2, value: 200.75 }, ] - const { lastFrame } = render() + const { lastFrame } = render() const output = lastFrame() expect(output).toContain("100.5") @@ -166,7 +166,7 @@ describe("DataGrid", () => { { id: 1, active: true }, { id: 2, active: false }, ] - const { lastFrame } = render() + const { lastFrame } = render() const output = lastFrame() expect(output).toContain("true") diff --git a/terminal/components/DataGrid.tsx b/terminal/components/Datagrid/Datagrid.tsx similarity index 99% rename from terminal/components/DataGrid.tsx rename to terminal/components/Datagrid/Datagrid.tsx index d522eb88..19c97ecd 100644 --- a/terminal/components/DataGrid.tsx +++ b/terminal/components/Datagrid/Datagrid.tsx @@ -10,7 +10,7 @@ const MAX_COLUMNS = 10 const MIN_COLUMN_WIDTH = 15 export type Order = { col: number; dir: "asc" | "desc" } -export function DataGrid(props: { +export function Datagrid(props: { records: DataRecord[] schema?: Schema col?: number diff --git a/terminal/components/Datagrid/index.ts b/terminal/components/Datagrid/index.ts new file mode 100644 index 00000000..cd9b0784 --- /dev/null +++ b/terminal/components/Datagrid/index.ts @@ -0,0 +1,2 @@ +export { Datagrid } from "./Datagrid.tsx" +export type { Order } from "./Datagrid.tsx" diff --git a/terminal/components/Dialect/Dialect.tsx b/terminal/components/Dialect/Dialect.tsx new file mode 100644 index 00000000..6bcd5323 --- /dev/null +++ b/terminal/components/Dialect/Dialect.tsx @@ -0,0 +1,12 @@ +import type * as library from "@dpkit/library" +import type { DataRecord } from "@dpkit/library" +import React from "react" +import { Datagrid } from "../Datagrid/index.ts" + +// TODO: Support non-visible chars like TAB and CR + +export function Dialect(props: { dialect: library.Dialect }) { + const records = [props.dialect as DataRecord] + + return +} diff --git a/terminal/components/Dialect/index.ts b/terminal/components/Dialect/index.ts new file mode 100644 index 00000000..86102e02 --- /dev/null +++ b/terminal/components/Dialect/index.ts @@ -0,0 +1 @@ +export { Dialect } from "./Dialect.tsx" diff --git a/terminal/components/DialectGrid.tsx b/terminal/components/DialectGrid.tsx deleted file mode 100644 index 7db3b319..00000000 --- a/terminal/components/DialectGrid.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Dialect } from "@dpkit/library" -import type { DataRecord } from "@dpkit/library" -import React from "react" -import { DataGrid } from "./DataGrid.tsx" - -// TODO: Support non-visible chars like TAB and CR - -export function DialectGrid(props: { dialect: Dialect }) { - const records = [props.dialect as DataRecord] - - return -} diff --git a/terminal/components/PackageGrid.tsx b/terminal/components/Package/Package.tsx similarity index 55% rename from terminal/components/PackageGrid.tsx rename to terminal/components/Package/Package.tsx index 68468101..6a76aae0 100644 --- a/terminal/components/PackageGrid.tsx +++ b/terminal/components/Package/Package.tsx @@ -1,10 +1,10 @@ -import type { Package } from "@dpkit/library" +import type * as library from "@dpkit/library" import React from "react" -import { DataGrid } from "./DataGrid.tsx" +import { Datagrid } from "../Datagrid/index.ts" // TODO: Support showing other package/resource properties -export function PackageGrid(props: { dataPackage: Package }) { +export function Package(props: { dataPackage: library.Package }) { const records = [ Object.fromEntries( props.dataPackage.resources.map(resource => [ @@ -14,5 +14,5 @@ export function PackageGrid(props: { dataPackage: Package }) { ), ] - return + return } diff --git a/terminal/components/Package/index.ts b/terminal/components/Package/index.ts new file mode 100644 index 00000000..aa830bdc --- /dev/null +++ b/terminal/components/Package/index.ts @@ -0,0 +1 @@ +export { Package } from "./Package.tsx" diff --git a/terminal/components/ErrorGrid.tsx b/terminal/components/Report/Report.tsx similarity index 72% rename from terminal/components/ErrorGrid.tsx rename to terminal/components/Report/Report.tsx index 14782f36..64019227 100644 --- a/terminal/components/ErrorGrid.tsx +++ b/terminal/components/Report/Report.tsx @@ -1,9 +1,9 @@ import type { UnboundError } from "@dpkit/library" import * as pl from "nodejs-polars" import React from "react" -import { TableGrid } from "./TableGrid.tsx" +import { Table } from "../Table/index.ts" -export function ErrorGrid(props: { +export function Report(props: { errors: UnboundError[] quit?: boolean }) { @@ -16,5 +16,5 @@ export function ErrorGrid(props: { const table = pl.DataFrame(errors).lazy() - return + return
} diff --git a/terminal/components/Report/index.ts b/terminal/components/Report/index.ts new file mode 100644 index 00000000..8171de19 --- /dev/null +++ b/terminal/components/Report/index.ts @@ -0,0 +1 @@ +export { Report } from "./Report.tsx" diff --git a/terminal/components/Resource/Resource.tsx b/terminal/components/Resource/Resource.tsx new file mode 100644 index 00000000..5400275a --- /dev/null +++ b/terminal/components/Resource/Resource.tsx @@ -0,0 +1,11 @@ +import type * as library from "@dpkit/library" +import React from "react" +import { Datagrid } from "../Datagrid/index.ts" + +// TODO: Support better display of resource properties + +export function Resource(props: { resource: library.Resource }) { + const { dialect, schema, ...record } = props.resource + + return +} diff --git a/terminal/components/Resource/index.ts b/terminal/components/Resource/index.ts new file mode 100644 index 00000000..9f56f1ba --- /dev/null +++ b/terminal/components/Resource/index.ts @@ -0,0 +1 @@ +export { Resource } from "./Resource.tsx" diff --git a/terminal/components/ResourceGrid.tsx b/terminal/components/ResourceGrid.tsx deleted file mode 100644 index 53621403..00000000 --- a/terminal/components/ResourceGrid.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { Resource } from "@dpkit/library" -import React from "react" -import { DataGrid } from "./DataGrid.tsx" - -// TODO: Support better display of resource properties - -export function ResourceGrid(props: { resource: Resource }) { - const { dialect, schema, ...record } = props.resource - - return -} diff --git a/terminal/components/SchemaGrid.tsx b/terminal/components/Schema/Schema.tsx similarity index 51% rename from terminal/components/SchemaGrid.tsx rename to terminal/components/Schema/Schema.tsx index 298d7164..d253922c 100644 --- a/terminal/components/SchemaGrid.tsx +++ b/terminal/components/Schema/Schema.tsx @@ -1,15 +1,15 @@ -import type { Schema } from "@dpkit/library" +import type * as library from "@dpkit/library" import React from "react" -import { DataGrid } from "./DataGrid.tsx" +import { Datagrid } from "../Datagrid/index.ts" // TODO: Support showing other schema/field properties -export function SchemaGrid(props: { schema: Schema }) { +export function Schema(props: { schema: library.Schema }) { const records = [ Object.fromEntries( props.schema.fields.map(field => [field.name, field.type]), ), ] - return + return } diff --git a/terminal/components/Schema/index.ts b/terminal/components/Schema/index.ts new file mode 100644 index 00000000..5b6279ee --- /dev/null +++ b/terminal/components/Schema/index.ts @@ -0,0 +1 @@ +export { Schema } from "./Schema.tsx" diff --git a/terminal/components/TableGrid.spec.tsx b/terminal/components/Table/Table.spec.tsx similarity index 83% rename from terminal/components/TableGrid.spec.tsx rename to terminal/components/Table/Table.spec.tsx index 8452a0ef..4f0516aa 100644 --- a/terminal/components/TableGrid.spec.tsx +++ b/terminal/components/Table/Table.spec.tsx @@ -2,9 +2,9 @@ import { render } from "ink-testing-library" import * as pl from "nodejs-polars" import React from "react" import { describe, expect, it } from "vitest" -import { TableGrid } from "./TableGrid.tsx" +import { Table } from "./Table.tsx" -describe("TableGrid", () => { +describe("Table", () => { it("should render basic table", async () => { const table = pl .DataFrame({ @@ -13,7 +13,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -38,7 +38,7 @@ describe("TableGrid", () => { ], } - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -55,7 +55,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -70,7 +70,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -85,7 +85,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -100,7 +100,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -118,7 +118,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -135,7 +135,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -153,7 +153,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -170,7 +170,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) @@ -187,7 +187,7 @@ describe("TableGrid", () => { }) .lazy() - const { lastFrame } = render() + const { lastFrame } = render(
) await new Promise(resolve => setTimeout(resolve, 100)) diff --git a/terminal/components/TableGrid.tsx b/terminal/components/Table/Table.tsx similarity index 93% rename from terminal/components/TableGrid.tsx rename to terminal/components/Table/Table.tsx index e8aedb1c..440a2d1d 100644 --- a/terminal/components/TableGrid.tsx +++ b/terminal/components/Table/Table.tsx @@ -1,19 +1,19 @@ -import type { DataRecord, Schema, Table } from "@dpkit/library" +import type * as library from "@dpkit/library" import { useApp, useInput } from "ink" import { Box, Text } from "ink" import pc from "picocolors" import { useEffect, useState } from "react" import React from "react" -import type { Order } from "./DataGrid.tsx" -import { DataGrid } from "./DataGrid.tsx" +import type { Order } from "../Datagrid/index.ts" +import { Datagrid } from "../Datagrid/index.ts" // TODO: Move components to their own folders const PAGE_SIZE = 10 -export function TableGrid(props: { - table: Table - schema?: Schema +export function Table(props: { + table: library.Table + schema?: library.Schema borderColor?: "green" | "red" withTypes?: boolean quit?: boolean @@ -25,7 +25,7 @@ export function TableGrid(props: { const [row, setRow] = useState(0) const [page, setPage] = useState(1) const [order, setOrder] = useState() - const [records, setRecords] = useState() + const [records, setRecords] = useState() const handleColChange = async (newCol: number) => { if (newCol <= 0) return @@ -134,7 +134,7 @@ export function TableGrid(props: { return ( - { + it("should return undefined when no options are provided", () => { + const result = createDialectFromOptions({}) + + expect(result).toBeUndefined() + }) + + it("should create dialect with delimiter", () => { + const result = createDialectFromOptions({ delimiter: "," }) + + expect(result).toEqual({ delimiter: "," }) + }) + + it("should create dialect with multiple options", () => { + const result = createDialectFromOptions({ + delimiter: ",", + quoteChar: '"', + escapeChar: "\\", + }) + + expect(result).toEqual({ + delimiter: ",", + quoteChar: '"', + escapeChar: "\\", + }) + }) + + it("should set header to false when explicitly set", () => { + const result = createDialectFromOptions({ header: false }) + + expect(result).toEqual({ header: false }) + }) + + it("should not set header when true", () => { + const result = createDialectFromOptions({ header: true }) + + expect(result).toBeUndefined() + }) + + it("should parse headerRows from comma-separated string", () => { + const result = createDialectFromOptions({ headerRows: "0,1,2" }) + + expect(result).toEqual({ headerRows: [0, 1, 2] }) + }) + + it("should set headerJoin", () => { + const result = createDialectFromOptions({ headerJoin: " " }) + + expect(result).toEqual({ headerJoin: " " }) + }) + + it("should parse commentRows from comma-separated string", () => { + const result = createDialectFromOptions({ commentRows: "0,5,10" }) + + expect(result).toEqual({ commentRows: [0, 5, 10] }) + }) + + it("should set commentChar", () => { + const result = createDialectFromOptions({ commentChar: "#" }) + + expect(result).toEqual({ commentChar: "#" }) + }) + + it("should set quoteChar", () => { + const result = createDialectFromOptions({ quoteChar: "'" }) + + expect(result).toEqual({ quoteChar: "'" }) + }) + + it("should set doubleQuote", () => { + const result = createDialectFromOptions({ doubleQuote: true }) + + expect(result).toEqual({ doubleQuote: true }) + }) + + it("should set escapeChar", () => { + const result = createDialectFromOptions({ escapeChar: "\\" }) + + expect(result).toEqual({ escapeChar: "\\" }) + }) + + it("should set nullSequence", () => { + const result = createDialectFromOptions({ nullSequence: "NULL" }) + + expect(result).toEqual({ nullSequence: "NULL" }) + }) + + it("should set skipInitialSpace", () => { + const result = createDialectFromOptions({ skipInitialSpace: true }) + + expect(result).toEqual({ skipInitialSpace: true }) + }) + + it("should set property", () => { + const result = createDialectFromOptions({ property: "data" }) + + expect(result).toEqual({ property: "data" }) + }) + + it("should set itemType", () => { + const result = createDialectFromOptions({ itemType: "object" }) + + expect(result).toEqual({ itemType: "object" }) + }) + + it("should parse itemKeys from comma-separated string", () => { + const result = createDialectFromOptions({ itemKeys: "id,name,email" }) + + expect(result).toEqual({ itemKeys: ["id", "name", "email"] }) + }) + + it("should set sheetNumber", () => { + const result = createDialectFromOptions({ sheetNumber: 1 }) + + expect(result).toEqual({ sheetNumber: 1 }) + }) + + it("should set sheetName", () => { + const result = createDialectFromOptions({ sheetName: "Sheet1" }) + + expect(result).toEqual({ sheetName: "Sheet1" }) + }) + + it("should set table", () => { + const result = createDialectFromOptions({ table: "users" }) + + expect(result).toEqual({ table: "users" }) + }) + + it("should handle complex CSV dialect", () => { + const result = createDialectFromOptions({ + delimiter: ";", + quoteChar: '"', + doubleQuote: true, + escapeChar: "\\", + header: false, + commentChar: "#", + }) + + expect(result).toEqual({ + delimiter: ";", + quoteChar: '"', + doubleQuote: true, + escapeChar: "\\", + header: false, + commentChar: "#", + }) + }) + + it("should handle JSON dialect", () => { + const result = createDialectFromOptions({ + property: "results", + itemType: "object", + itemKeys: "id,name,email", + }) + + expect(result).toEqual({ + property: "results", + itemType: "object", + itemKeys: ["id", "name", "email"], + }) + }) + + it("should handle spreadsheet dialect", () => { + const result = createDialectFromOptions({ + sheetName: "Data", + headerRows: "0,1", + headerJoin: " ", + }) + + expect(result).toEqual({ + sheetName: "Data", + headerRows: [0, 1], + headerJoin: " ", + }) + }) + + it("should handle database dialect", () => { + const result = createDialectFromOptions({ + table: "users", + }) + + expect(result).toEqual({ + table: "users", + }) + }) +}) + +describe("createToDialectFromOptions", () => { + it("should return undefined when no options are provided", () => { + const result = createToDialectFromOptions({}) + + expect(result).toBeUndefined() + }) + + it("should create dialect with toDelimiter", () => { + const result = createToDialectFromOptions({ toDelimiter: "," }) + + expect(result).toEqual({ delimiter: "," }) + }) + + it("should create dialect with multiple to-prefixed options", () => { + const result = createToDialectFromOptions({ + toDelimiter: ",", + toQuoteChar: '"', + toEscapeChar: "\\", + }) + + expect(result).toEqual({ + delimiter: ",", + quoteChar: '"', + escapeChar: "\\", + }) + }) + + it("should set header to false when toHeader explicitly set", () => { + const result = createToDialectFromOptions({ toHeader: false }) + + expect(result).toEqual({ header: false }) + }) + + it("should not set header when toHeader is true", () => { + const result = createToDialectFromOptions({ toHeader: true }) + + expect(result).toBeUndefined() + }) + + it("should parse toHeaderRows from comma-separated string", () => { + const result = createToDialectFromOptions({ toHeaderRows: "0,1,2" }) + + expect(result).toEqual({ headerRows: [0, 1, 2] }) + }) + + it("should set toHeaderJoin", () => { + const result = createToDialectFromOptions({ toHeaderJoin: " " }) + + expect(result).toEqual({ headerJoin: " " }) + }) + + it("should parse toCommentRows from comma-separated string", () => { + const result = createToDialectFromOptions({ toCommentRows: "0,5,10" }) + + expect(result).toEqual({ commentRows: [0, 5, 10] }) + }) + + it("should set toCommentChar", () => { + const result = createToDialectFromOptions({ toCommentChar: "#" }) + + expect(result).toEqual({ commentChar: "#" }) + }) + + it("should set toQuoteChar", () => { + const result = createToDialectFromOptions({ toQuoteChar: "'" }) + + expect(result).toEqual({ quoteChar: "'" }) + }) + + it("should set toDoubleQuote", () => { + const result = createToDialectFromOptions({ toDoubleQuote: true }) + + expect(result).toEqual({ doubleQuote: true }) + }) + + it("should set toEscapeChar", () => { + const result = createToDialectFromOptions({ toEscapeChar: "\\" }) + + expect(result).toEqual({ escapeChar: "\\" }) + }) + + it("should set toNullSequence", () => { + const result = createToDialectFromOptions({ toNullSequence: "NULL" }) + + expect(result).toEqual({ nullSequence: "NULL" }) + }) + + it("should set toSkipInitialSpace", () => { + const result = createToDialectFromOptions({ toSkipInitialSpace: true }) + + expect(result).toEqual({ skipInitialSpace: true }) + }) + + it("should set toProperty", () => { + const result = createToDialectFromOptions({ toProperty: "data" }) + + expect(result).toEqual({ property: "data" }) + }) + + it("should set toItemType", () => { + const result = createToDialectFromOptions({ toItemType: "object" }) + + expect(result).toEqual({ itemType: "object" }) + }) + + it("should parse toItemKeys from comma-separated string", () => { + const result = createToDialectFromOptions({ toItemKeys: "id,name,email" }) + + expect(result).toEqual({ itemKeys: ["id", "name", "email"] }) + }) + + it("should set toSheetNumber", () => { + const result = createToDialectFromOptions({ toSheetNumber: 1 }) + + expect(result).toEqual({ sheetNumber: 1 }) + }) + + it("should set toSheetName", () => { + const result = createToDialectFromOptions({ toSheetName: "Sheet1" }) + + expect(result).toEqual({ sheetName: "Sheet1" }) + }) + + it("should set toTable", () => { + const result = createToDialectFromOptions({ toTable: "users" }) + + expect(result).toEqual({ table: "users" }) + }) + + it("should handle complex CSV to-dialect", () => { + const result = createToDialectFromOptions({ + toDelimiter: ";", + toQuoteChar: '"', + toDoubleQuote: true, + toEscapeChar: "\\", + toHeader: false, + toCommentChar: "#", + }) + + expect(result).toEqual({ + delimiter: ";", + quoteChar: '"', + doubleQuote: true, + escapeChar: "\\", + header: false, + commentChar: "#", + }) + }) + + it("should handle JSON to-dialect", () => { + const result = createToDialectFromOptions({ + toProperty: "results", + toItemType: "object", + toItemKeys: "id,name,email", + }) + + expect(result).toEqual({ + property: "results", + itemType: "object", + itemKeys: ["id", "name", "email"], + }) + }) + + it("should handle spreadsheet to-dialect", () => { + const result = createToDialectFromOptions({ + toSheetName: "Data", + toHeaderRows: "0,1", + toHeaderJoin: " ", + }) + + expect(result).toEqual({ + sheetName: "Data", + headerRows: [0, 1], + headerJoin: " ", + }) + }) + + it("should handle database to-dialect", () => { + const result = createToDialectFromOptions({ + toTable: "users", + }) + + expect(result).toEqual({ + table: "users", + }) + }) + + it("should not mix source and to options", () => { + const result = createToDialectFromOptions({ + delimiter: ",", + toDelimiter: ";", + }) + + expect(result).toEqual({ delimiter: ";" }) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts index 7fd91fe7..4cb5b835 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -26,7 +26,9 @@ export default defineConfig({ "**/coverage/**", "**/entrypoints/**", "**/examples/**", + "**/program.ts", "**/index.ts", + "**/main.ts", "browser/**", "docs/**", "service/**",