From 76c680166655a770d142e1475225ee5e5fb17772 Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 07:58:06 -0500 Subject: [PATCH 01/13] chore: add nocodb package and update cspell dictionary --- .config/.cspell.json | 1 + README.md | 1 + core/typed-nocodb-api/README.md | 28 +++++ core/typed-nocodb-api/example/index.ts | 27 +++++ core/typed-nocodb-api/package.json | 49 +++++++++ core/typed-nocodb-api/src/index.ts | 141 +++++++++++++++++++++++++ core/typed-nocodb-api/tsconfig.json | 4 + core/typed-nocodb-api/tsdown.config.ts | 11 ++ core/typed-nocodb-api/typedoc.json | 12 +++ eslint.config.ts | 2 +- package.json | 2 +- pnpm-lock.yaml | 139 ++++++++++++++++++++++-- 12 files changed, 406 insertions(+), 11 deletions(-) create mode 100644 core/typed-nocodb-api/README.md create mode 100644 core/typed-nocodb-api/example/index.ts create mode 100644 core/typed-nocodb-api/package.json create mode 100644 core/typed-nocodb-api/src/index.ts create mode 100644 core/typed-nocodb-api/tsconfig.json create mode 100644 core/typed-nocodb-api/tsdown.config.ts create mode 100644 core/typed-nocodb-api/typedoc.json diff --git a/.config/.cspell.json b/.config/.cspell.json index cda58a5f..31057475 100644 --- a/.config/.cspell.json +++ b/.config/.cspell.json @@ -26,6 +26,7 @@ "macchiato", "manypkg", "multipublish", + "nocodb", "nodemon", "nvim", "nvmrc", diff --git a/README.md b/README.md index 782e477f..9d2f4ea3 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ All packages are packaged underneath the `@stephansama` scope (for example: `@st | [remark-asciinema](core/remark-asciinema/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fremark-asciinema?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/remark-asciinema?labelColor=211F1F) | A remark plugin that transforms Asciinema links into embedded players or screenshots. | | [svelte-social-share-links](core/svelte-social-share-links/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fsvelte-social-share-links?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/svelte-social-share-links?labelColor=211F1F) | Svelte/Web component to share the current url with various social media providers | | [typed-events](core/typed-events/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-events?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-events?labelColor=211F1F) | Typed events store using standard schema | +| [typed-nocodb-api](core/typed-nocodb-api/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-nocodb-api?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-nocodb-api?labelColor=211F1F) | zod nocodb api | | [typed-templates](core/typed-templates/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-templates?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-templates?labelColor=211F1F) | Use standard schema to validate and use handlebar template directories | diff --git a/core/typed-nocodb-api/README.md b/core/typed-nocodb-api/README.md new file mode 100644 index 00000000..7ebd9abd --- /dev/null +++ b/core/typed-nocodb-api/README.md @@ -0,0 +1,28 @@ +# @stephansama/typed-nocodb-api + +[![Source code](https://img.shields.io/badge/Source-666666?style=flat&logo=github&label=Github&labelColor=211F1F)](https://github.com/stephansama/packages/tree/main/core/typed-nocodb-api) +[![Documentation](https://img.shields.io/badge/Documentation-211F1F?style=flat&logo=Wikibooks&labelColor=211F1F)](https://packages.stephansama.info/api/@stephansama/typed-nocodb-api) +[![NPM Version](https://img.shields.io/npm/v/%40stephansama%2Ftyped-nocodb-api?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F)](https://www.npmjs.com/package/@stephansama/typed-nocodb-api) +[![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-nocodb-api?labelColor=211F1F)](https://www.npmjs.com/package/@stephansama/typed-nocodb-api) + +standard schema compatible nocodb api + +##### Table of contents + +
Open Table of contents + +- [Installation](#installation) +- [Usage](#usage) + +
+ +## Installation + +```sh +pnpm install @stephansama/typed-nocodb-api +``` + +## Usage + +> \[!CAUTION] +> WIP diff --git a/core/typed-nocodb-api/example/index.ts b/core/typed-nocodb-api/example/index.ts new file mode 100644 index 00000000..d5881879 --- /dev/null +++ b/core/typed-nocodb-api/example/index.ts @@ -0,0 +1,27 @@ +// remark-usage-ignore-next +/* eslint perfectionist/sort-modules: ["off"] */ +// remark-usage-ignore-next +/* eslint perfectionist/sort-imports: ["off"] */ +// remark-usage-ignore-next +import * as z from "zod"; + +import { createApi } from "../dist/index.cjs"; + +const api = createApi({ + baseId: process.env.NOCODB_BASE!, + origin: "https://nocodb.com", + schema: z.object({ + column1: z.string(), + column2: z.enum(["optionOne", "optionTwo", "optionThree"]), + column3: z.number(), + column4: z.boolean(), + }), + tableId: process.env.NOCODB_TABLE!, + token: process.env.NOCODB_TOKEN, +}); + +export function callApi() { + api.fetch({ + action: "LIST", + }); +} diff --git a/core/typed-nocodb-api/package.json b/core/typed-nocodb-api/package.json new file mode 100644 index 00000000..bb659e28 --- /dev/null +++ b/core/typed-nocodb-api/package.json @@ -0,0 +1,49 @@ +{ + "name": "@stephansama/typed-nocodb-api", + "version": "0.0.0", + "description": "zod nocodb api", + "keywords": [ + "typed-nocodb-api" + ], + "homepage": "https://packages.stephansama.info/api/@stephansama/typed-nocodb-api", + "repository": { + "type": "git", + "url": "https://github.com/stephansama/packages", + "directory": "core/typed-nocodb-api" + }, + "license": "MIT", + "author": { + "name": "Stephan Randle", + "email": "stephanrandle.dev@gmail.com", + "url": "https://stephansama.info" + }, + "type": "module", + "scripts": { + "build": "tsdown", + "dev": "tsdown --watch", + "lint": "eslint ./ --pass-on-no-patterns --no-error-on-unmatched-pattern" + }, + "dependencies": {}, + "devDependencies": { + "tsdown": "catalog:", + "zod": "catalog:schema" + }, + "peerDependencies": { + "zod": "catalog:schema" + }, + "packageManager": "pnpm@10.11.0", + "publishConfig": { + "access": "public", + "provenance": true + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + } +} diff --git a/core/typed-nocodb-api/src/index.ts b/core/typed-nocodb-api/src/index.ts new file mode 100644 index 00000000..7009b5d8 --- /dev/null +++ b/core/typed-nocodb-api/src/index.ts @@ -0,0 +1,141 @@ +import * as z from "zod"; + +export const ACTIONS = [ + "LIST", + "CREATE", + "UPDATE", + "DELETE", + "READ", + "COUNT", +] as const; +export type ACTION = (typeof ACTIONS)[number]; + +export function createApi({ + baseId, + origin, + schema, + tableId, + token, +}: { + baseId: string; + origin: string; + schema: Schema; + tableId: string; + token?: string; +}) { + let _token: string | undefined = token; + + const api = { + COUNT: { + method: "get", + responseSchema: z + .object({ count: z.number() }) + .or(z.object({ msg: z.string() })), + url: `/api/v3/data/${baseId}/${tableId}/records`, + }, + CREATE: { + inputSchema: z.object({ fields: schema }), + method: "post", + responseSchema: z.object({ + records: z.array(z.object({ fields: schema, id: z.string() })), + }), + url: `/api/v3/data/${baseId}/${tableId}/records`, + }, + DELETE: { + inputSchema: z.object({ id: z.number() }), + method: "patch", + responseSchema: z.object(), + url: `/api/v3/data/${baseId}/${tableId}/records`, + }, + LIST: { + method: "get", + querySchema: z.object({ + fields: z.array(z.string()).or(z.string()), + sort: z + .object({ + direction: z.enum(["asc", "desc"]), + field: z.string(), + }) + .transform((input) => JSON.stringify(input)), + }), + responseSchema: z.object({ + nestedNext: z.string().optional().nullable(), + nestedPrev: z.string().optional().nullable(), + next: z.string().optional().nullable(), + prev: z.string().optional().nullable(), + records: z.array(z.object({ fields: schema, id: z.number() })), + }), + url: `/api/v3/data/${baseId}/${tableId}/records`, + }, + READ: { + method: "get", + responseSchema: z.object({ fields: schema, id: z.number() }), + url: `/api/v3/data/${baseId}/${tableId}/records/{recordId}`, + }, + UPDATE: { + inputSchema: z.object({ fields: schema, id: z.string() }), + method: "patch", + responseSchema: z.object(), + url: `/api/v3/data/${baseId}/${tableId}/records`, + }, + } satisfies Record< + ACTION, + { + inputSchema?: z.ZodType; + method: "delete" | "get" | "patch" | "post" | "put"; + querySchema?: z.ZodType; + responseSchema: z.ZodType; + url: string; + } + >; + + type API = typeof api; + + return { + async fetch( + props: { + [A in ACTION]: ("inputSchema" extends keyof API[A] + ? { body: z.infer } + : {}) & + ("querySchema" extends keyof API[A] + ? { query?: z.infer } + : {}) & { + action: A; + token?: string; + }; + }[ACTION], + ) { + const token = (_token ??= props.token); + if (!token) throw new Error("no token provided"); + + const current = api[props.action]; + + const url = new URL(current.url, origin); + + let params = ""; + + if ("query" in props && "querySchema" in current) { + const parsed = current.querySchema.parse(props.query); + params = "?" + new URLSearchParams(parsed).toString(); + } + + let body: string | undefined; + + if ("body" in props && "inputSchema" in current) { + body = JSON.stringify(current.inputSchema.parse(props.body)); + } + + const response = await fetch(url + params, { + body, + headers: new Headers({ + "accept": "application/json", + "xc-token": token, + }), + method: current.method, + }); + + const json = await response.json(); + return current.responseSchema.parse(json); + }, + }; +} diff --git a/core/typed-nocodb-api/tsconfig.json b/core/typed-nocodb-api/tsconfig.json new file mode 100644 index 00000000..bd3334a8 --- /dev/null +++ b/core/typed-nocodb-api/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../tsconfig.base.json"], + "include": ["./src/**/*"] +} diff --git a/core/typed-nocodb-api/tsdown.config.ts b/core/typed-nocodb-api/tsdown.config.ts new file mode 100644 index 00000000..a1058fc6 --- /dev/null +++ b/core/typed-nocodb-api/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + attw: true, + dts: true, + entry: ["src/index.ts"], + exports: true, + format: ["esm", "cjs"], + publint: true, + target: "esnext", +}); diff --git a/core/typed-nocodb-api/typedoc.json b/core/typed-nocodb-api/typedoc.json new file mode 100644 index 00000000..57eeff2c --- /dev/null +++ b/core/typed-nocodb-api/typedoc.json @@ -0,0 +1,12 @@ +{ + "entryPoints": ["src/*"], + "tsconfig": "./tsconfig.json", + "exclude": [ + "**/tests/**", + "**/*.test.ts", + "**/*.spec.ts", + "node_modules", + "**/{node_modules,test,book,doc,dist}/**/*", + "**/{pages,components}/**" + ] +} diff --git a/eslint.config.ts b/eslint.config.ts index f65aeae0..0ad213bc 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -9,7 +9,7 @@ import pluginPnpm from "eslint-plugin-pnpm"; import eslintPluginReactHooks from "eslint-plugin-react-hooks"; import storybook from "eslint-plugin-storybook"; import testingLibrary from "eslint-plugin-testing-library"; -import eslintPluginZodX from "eslint-plugin-zod-x"; +import eslintPluginZodX from "eslint-plugin-zod"; import { defineConfig } from "eslint/config"; import globals from "globals"; import * as jsoncParser from "jsonc-eslint-parser"; diff --git a/package.json b/package.json index d0c56ae4..608f1839 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-storybook": "^10.1.4", "eslint-plugin-testing-library": "^7.13.5", - "eslint-plugin-zod-x": "^1.13.2", + "eslint-plugin-zod": "^3.0.0", "globals": "^16.5.0", "husky": "^9.1.7", "jsdom": "^27.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aaafba2b..239f2d57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,9 +264,9 @@ importers: eslint-plugin-testing-library: specifier: ^7.13.5 version: 7.13.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-zod-x: - specifier: ^1.13.2 - version: 1.13.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(zod@4.0.15) + eslint-plugin-zod: + specifier: ^3.0.0 + version: 3.1.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(zod@4.0.15) globals: specifier: ^16.5.0 version: 16.5.0 @@ -722,6 +722,19 @@ importers: specifier: '>=18' version: 19.2.0 + core/typed-nocodb-api: + dependencies: + '@standard-schema/spec': + specifier: catalog:schema + version: 1.1.0 + devDependencies: + tsdown: + specifier: 'catalog:' + version: 0.15.12(@arethetypeswrong/core@0.18.2)(oxc-resolver@11.14.2)(publint@0.3.15)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)) + zod: + specifier: catalog:schema + version: 4.2.1 + core/typed-templates: dependencies: '@standard-schema/spec': @@ -2548,6 +2561,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -4154,6 +4173,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.55.0': + resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@8.39.1': resolution: {integrity: sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4162,6 +4187,10 @@ packages: resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.55.0': + resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.39.1': resolution: {integrity: sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4174,6 +4203,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.55.0': + resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.48.1': resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4193,6 +4228,10 @@ packages: resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.55.0': + resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.39.1': resolution: {integrity: sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4205,6 +4244,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.55.0': + resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.39.1': resolution: {integrity: sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4219,6 +4264,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.55.0': + resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@8.39.1': resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4227,6 +4279,10 @@ packages: resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.55.0': + resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -5809,10 +5865,9 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 - eslint-plugin-zod-x@1.13.2: - resolution: {integrity: sha512-Zw3PkNvKDp2H3wfrSwh+ydbf6WtkKlMqFEzyAN+cxxueiZRj/AtJkdjmll2IFYnOSi+2VBVV9quqDUfZItcNLQ==} + eslint-plugin-zod@3.1.0: + resolution: {integrity: sha512-9XemRkKexNSXP31yDGNlghr0c42hWJaqgmQ1tQx+u4ZawxObVlSQyKTTATXmN7Cr4BXH2uUrKYiZoEhLhoAXrg==} engines: {node: ^20 || ^22 || >=24} - deprecated: eslint-plugin-zod-x is deprecated. Use eslint-plugin-zod >=3. See https://github.com/marcalexiei/eslint-plugin-zod/releases/tag/v3.0.0 peerDependencies: eslint: ^9 zod: ^4 @@ -6209,7 +6264,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -9268,6 +9323,12 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -11957,6 +12018,11 @@ snapshots: eslint: 9.39.1(jiti@2.6.1) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.1(jiti@2.6.1))': + dependencies: + eslint: 9.39.1(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} '@eslint-community/regexpp@4.12.2': {} @@ -13545,6 +13611,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.39.1': dependencies: '@typescript-eslint/types': 8.39.1 @@ -13555,6 +13630,11 @@ snapshots: '@typescript-eslint/types': 8.48.1 '@typescript-eslint/visitor-keys': 8.48.1 + '@typescript-eslint/scope-manager@8.55.0': + dependencies: + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 + '@typescript-eslint/tsconfig-utils@8.39.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -13563,6 +13643,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.48.1 @@ -13581,6 +13665,8 @@ snapshots: '@typescript-eslint/types@8.48.1': {} + '@typescript-eslint/types@8.55.0': {} + '@typescript-eslint/typescript-estree@8.39.1(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.39.1(typescript@5.9.3) @@ -13612,6 +13698,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.39.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.39.1(jiti@2.6.1)) @@ -13634,6 +13735,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.55.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.39.1': dependencies: '@typescript-eslint/types': 8.39.1 @@ -13644,6 +13756,11 @@ snapshots: '@typescript-eslint/types': 8.48.1 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.55.0': + dependencies: + '@typescript-eslint/types': 8.55.0 + eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} '@vercel/oidc@3.0.5': {} @@ -15742,9 +15859,9 @@ snapshots: - supports-color - typescript - eslint-plugin-zod-x@1.13.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(zod@4.0.15): + eslint-plugin-zod@3.1.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(zod@4.0.15): dependencies: - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) esquery: 1.6.0 optionalDependencies: @@ -20029,6 +20146,10 @@ snapshots: dependencies: typescript: 5.9.3 + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-dedent@2.2.0: {} ts-node@10.9.2(@swc/core@1.12.1(@swc/helpers@0.5.17))(@types/node@24.10.1)(typescript@5.9.3): From d8efe8564a8032d506ae23f06d76006a879cc46a Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 07:58:57 -0500 Subject: [PATCH 02/13] chore: remove unused dependency @standard-schema/spec from pnpm lock file --- pnpm-lock.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 239f2d57..4ffac94d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -723,10 +723,6 @@ importers: version: 19.2.0 core/typed-nocodb-api: - dependencies: - '@standard-schema/spec': - specifier: catalog:schema - version: 1.1.0 devDependencies: tsdown: specifier: 'catalog:' From 05c6dfd202458e2dd4ad0cf2098032c8f9f2433f Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 07:59:57 -0500 Subject: [PATCH 03/13] chore: rename example file to use js instead of ts --- core/typed-nocodb-api/example/{index.ts => index.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/typed-nocodb-api/example/{index.ts => index.js} (100%) diff --git a/core/typed-nocodb-api/example/index.ts b/core/typed-nocodb-api/example/index.js similarity index 100% rename from core/typed-nocodb-api/example/index.ts rename to core/typed-nocodb-api/example/index.js From cbe0c5c3afececbd38e61702d5c361672fd032d0 Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:01:21 -0500 Subject: [PATCH 04/13] chore: update readme and example to remove exclamation marks from env variables --- core/typed-nocodb-api/README.md | 24 ++++++++++++++++++++++-- core/typed-nocodb-api/example/index.js | 4 ++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/core/typed-nocodb-api/README.md b/core/typed-nocodb-api/README.md index 7ebd9abd..8106f56c 100644 --- a/core/typed-nocodb-api/README.md +++ b/core/typed-nocodb-api/README.md @@ -24,5 +24,25 @@ pnpm install @stephansama/typed-nocodb-api ## Usage -> \[!CAUTION] -> WIP +```javascript +import { createApi } from "@stephansama/typed-nocodb-api"; + +const api = createApi({ + baseId: process.env.NOCODB_BASE, + origin: "https://nocodb.com", + schema: z.object({ + column1: z.string(), + column2: z.enum(["optionOne", "optionTwo", "optionThree"]), + column3: z.number(), + column4: z.boolean(), + }), + tableId: process.env.NOCODB_TABLE, + token: process.env.NOCODB_TOKEN, +}); + +export function callApi() { + api.fetch({ + action: "LIST", + }); +} +``` diff --git a/core/typed-nocodb-api/example/index.js b/core/typed-nocodb-api/example/index.js index d5881879..8e9b4814 100644 --- a/core/typed-nocodb-api/example/index.js +++ b/core/typed-nocodb-api/example/index.js @@ -8,7 +8,7 @@ import * as z from "zod"; import { createApi } from "../dist/index.cjs"; const api = createApi({ - baseId: process.env.NOCODB_BASE!, + baseId: process.env.NOCODB_BASE, origin: "https://nocodb.com", schema: z.object({ column1: z.string(), @@ -16,7 +16,7 @@ const api = createApi({ column3: z.number(), column4: z.boolean(), }), - tableId: process.env.NOCODB_TABLE!, + tableId: process.env.NOCODB_TABLE, token: process.env.NOCODB_TOKEN, }); From 343e42bfcaf981ff8770d5fb0daade4d218fdbec Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:01:50 -0500 Subject: [PATCH 05/13] chore: add eslint disable for sort imports in example and readme --- core/typed-nocodb-api/README.md | 4 ++++ core/typed-nocodb-api/example/index.js | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/typed-nocodb-api/README.md b/core/typed-nocodb-api/README.md index 8106f56c..738b3701 100644 --- a/core/typed-nocodb-api/README.md +++ b/core/typed-nocodb-api/README.md @@ -24,6 +24,10 @@ pnpm install @stephansama/typed-nocodb-api ## Usage +```javascript +/* eslint perfectionist/sort-imports: ["off"] */ +``` + ```javascript import { createApi } from "@stephansama/typed-nocodb-api"; diff --git a/core/typed-nocodb-api/example/index.js b/core/typed-nocodb-api/example/index.js index 8e9b4814..23820392 100644 --- a/core/typed-nocodb-api/example/index.js +++ b/core/typed-nocodb-api/example/index.js @@ -1,6 +1,3 @@ -// remark-usage-ignore-next -/* eslint perfectionist/sort-modules: ["off"] */ -// remark-usage-ignore-next /* eslint perfectionist/sort-imports: ["off"] */ // remark-usage-ignore-next import * as z from "zod"; From 243432d559496131a66091fec16e79dfafaf3392 Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:02:26 -0500 Subject: [PATCH 06/13] chore: remove eslint disable and remark ignore comments in example and readme --- core/typed-nocodb-api/README.md | 4 +--- core/typed-nocodb-api/example/index.js | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/core/typed-nocodb-api/README.md b/core/typed-nocodb-api/README.md index 738b3701..bd531f97 100644 --- a/core/typed-nocodb-api/README.md +++ b/core/typed-nocodb-api/README.md @@ -25,10 +25,8 @@ pnpm install @stephansama/typed-nocodb-api ## Usage ```javascript -/* eslint perfectionist/sort-imports: ["off"] */ -``` +import * as z from "zod"; -```javascript import { createApi } from "@stephansama/typed-nocodb-api"; const api = createApi({ diff --git a/core/typed-nocodb-api/example/index.js b/core/typed-nocodb-api/example/index.js index 23820392..4ed370fa 100644 --- a/core/typed-nocodb-api/example/index.js +++ b/core/typed-nocodb-api/example/index.js @@ -1,5 +1,3 @@ -/* eslint perfectionist/sort-imports: ["off"] */ -// remark-usage-ignore-next import * as z from "zod"; import { createApi } from "../dist/index.cjs"; From fb0108822178e0cb207aae6af7a8b63ce1023e23 Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:04:26 -0500 Subject: [PATCH 07/13] chore: update schema types to use z.array(z.string) instead of z.string().array() --- core/auto-readme/src/schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/auto-readme/src/schema.ts b/core/auto-readme/src/schema.ts index ce50df8c..4c99540a 100644 --- a/core/auto-readme/src/schema.ts +++ b/core/auto-readme/src/schema.ts @@ -65,8 +65,8 @@ export const defaultTemplates = templatesSchema.parse({}); export const defaultTableHeadings = tableHeadingsSchema.parse(undefined); const _configSchema = z.object({ - affectedRegexes: z.string().array().default([]), - collapseHeadings: z.string().array().default([]), + affectedRegexes: z.array(z.string()), + collapseHeadings: z.array(z.string()), defaultLanguage: languageSchema.meta({ alias: "l", description: "Default language to infer projects from", From 740e2032186f16c97780a963d3bbab08857a7014 Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:09:29 -0500 Subject: [PATCH 08/13] chore: addressed typescript issue --- core/multipublish/src/release.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/multipublish/src/release.ts b/core/multipublish/src/release.ts index 7b90947b..5942e2b4 100644 --- a/core/multipublish/src/release.ts +++ b/core/multipublish/src/release.ts @@ -52,5 +52,6 @@ export async function loadReleases(args: Args) { } const input = await readStdin(); + if (!input) throw new Error("no piped input provided"); return releasesSchema.parse(JSON.parse(input)); } From 6464044379b41b7f95882a1433376b71e2ab5b0a Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:15:32 -0500 Subject: [PATCH 09/13] chore: add story warnings --- core/auto-readme/src/args.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/auto-readme/src/args.ts b/core/auto-readme/src/args.ts index 7cc5e455..2a06ac3a 100644 --- a/core/auto-readme/src/args.ts +++ b/core/auto-readme/src/args.ts @@ -56,8 +56,10 @@ function zodToYargs(): Omit< const { shape } = configSchema.unwrap(); const entries = Object.entries(shape).map(([key, value]) => { if (complexOptions.includes(key as ComplexOptions)) return []; + // @ts-expect-error STE-71 if (value.def.innerType instanceof z.ZodObject) return []; const meta = value.meta(); + // @ts-expect-error STE-71 const { innerType } = value.def; const isBoolean = innerType instanceof z.ZodBoolean; const isNumber = innerType instanceof z.ZodNumber; @@ -70,6 +72,7 @@ function zodToYargs(): Omit< "string"; const options: Options = { + // @ts-expect-error STE-71 default: value.def.defaultValue, type: yargType, }; From 40f22c8f2465c4386e7f8db37cf714ea91d278d8 Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:19:59 -0500 Subject: [PATCH 10/13] chore: refactor fetch method to use fetchoptions type for better readability and maintainability --- core/typed-nocodb-api/src/index.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/core/typed-nocodb-api/src/index.ts b/core/typed-nocodb-api/src/index.ts index 7009b5d8..76232007 100644 --- a/core/typed-nocodb-api/src/index.ts +++ b/core/typed-nocodb-api/src/index.ts @@ -91,20 +91,20 @@ export function createApi({ type API = typeof api; + type FetchOptions = { + [A in ACTION]: ("inputSchema" extends keyof API[A] + ? { body: z.infer } + : {}) & + ("querySchema" extends keyof API[A] + ? { query?: z.infer } + : {}) & { + action: A; + token?: string; + }; + }[ACTION]; + return { - async fetch( - props: { - [A in ACTION]: ("inputSchema" extends keyof API[A] - ? { body: z.infer } - : {}) & - ("querySchema" extends keyof API[A] - ? { query?: z.infer } - : {}) & { - action: A; - token?: string; - }; - }[ACTION], - ) { + async fetch(props: FetchOptions) { const token = (_token ??= props.token); if (!token) throw new Error("no token provided"); From e215a765c4f4b38bfc0c365219cae1ad5264842e Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:38:10 -0500 Subject: [PATCH 11/13] chore: update typed-nocodb-api description and add test coverage --- README.md | 2 +- core/typed-nocodb-api/package.json | 22 +-- core/typed-nocodb-api/src/index.ts | 17 ++- core/typed-nocodb-api/test/index.test.ts | 181 +++++++++++++++++++++++ 4 files changed, 206 insertions(+), 16 deletions(-) create mode 100644 core/typed-nocodb-api/test/index.test.ts diff --git a/README.md b/README.md index 9d2f4ea3..88c42af8 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ All packages are packaged underneath the `@stephansama` scope (for example: `@st | [remark-asciinema](core/remark-asciinema/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fremark-asciinema?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/remark-asciinema?labelColor=211F1F) | A remark plugin that transforms Asciinema links into embedded players or screenshots. | | [svelte-social-share-links](core/svelte-social-share-links/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fsvelte-social-share-links?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/svelte-social-share-links?labelColor=211F1F) | Svelte/Web component to share the current url with various social media providers | | [typed-events](core/typed-events/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-events?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-events?labelColor=211F1F) | Typed events store using standard schema | -| [typed-nocodb-api](core/typed-nocodb-api/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-nocodb-api?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-nocodb-api?labelColor=211F1F) | zod nocodb api | +| [typed-nocodb-api](core/typed-nocodb-api/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-nocodb-api?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-nocodb-api?labelColor=211F1F) | Typed API client for NocoDB using Zod | | [typed-templates](core/typed-templates/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-templates?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-templates?labelColor=211F1F) | Use standard schema to validate and use handlebar template directories | diff --git a/core/typed-nocodb-api/package.json b/core/typed-nocodb-api/package.json index bb659e28..af0c7767 100644 --- a/core/typed-nocodb-api/package.json +++ b/core/typed-nocodb-api/package.json @@ -1,7 +1,7 @@ { "name": "@stephansama/typed-nocodb-api", "version": "0.0.0", - "description": "zod nocodb api", + "description": "Typed API client for NocoDB using Zod", "keywords": [ "typed-nocodb-api" ], @@ -18,6 +18,16 @@ "url": "https://stephansama.info" }, "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.cts", "scripts": { "build": "tsdown", "dev": "tsdown --watch", @@ -35,15 +45,5 @@ "publishConfig": { "access": "public", "provenance": true - }, - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.cts", - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.cjs" - }, - "./package.json": "./package.json" } } diff --git a/core/typed-nocodb-api/src/index.ts b/core/typed-nocodb-api/src/index.ts index 76232007..ca77bd11 100644 --- a/core/typed-nocodb-api/src/index.ts +++ b/core/typed-nocodb-api/src/index.ts @@ -112,6 +112,11 @@ export function createApi({ const url = new URL(current.url, origin); + const headers = new Headers({ + "accept": "application/json", + "xc-token": token, + }); + let params = ""; if ("query" in props && "querySchema" in current) { @@ -123,17 +128,21 @@ export function createApi({ if ("body" in props && "inputSchema" in current) { body = JSON.stringify(current.inputSchema.parse(props.body)); + headers.append("Content-Type", "application/json"); } const response = await fetch(url + params, { body, - headers: new Headers({ - "accept": "application/json", - "xc-token": token, - }), + headers, method: current.method, }); + if (!response.ok) { + throw new Error( + `failed to query nocodb ${response.statusText}`, + ); + } + const json = await response.json(); return current.responseSchema.parse(json); }, diff --git a/core/typed-nocodb-api/test/index.test.ts b/core/typed-nocodb-api/test/index.test.ts new file mode 100644 index 00000000..78be9406 --- /dev/null +++ b/core/typed-nocodb-api/test/index.test.ts @@ -0,0 +1,181 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { createApi } from '../src/index'; +import * as z from 'zod'; + +const schema = z.object({ + title: z.string(), + completed: z.boolean(), +}); + +describe('typed-nocodb-api', () => { + const baseId = 'baseId'; + const origin = 'http://localhost:8080'; + const tableId = 'tableId'; + const token = 'test-token'; + + const api = createApi({ + baseId, + origin, + schema, + tableId, + token, + }); + + const mockFetch = vi.fn(); + + beforeEach(() => { + vi.stubGlobal('fetch', mockFetch); + mockFetch.mockReset(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('should throw if no token is provided', async () => { + const apiNoToken = createApi({ + baseId, + origin, + schema, + tableId, + }); + + // @ts-expect-error - testing runtime check + await expect(apiNoToken.fetch({ action: 'LIST' })).rejects.toThrow('no token provided'); + }); + + it('should perform LIST action', async () => { + const mockResponse = { + records: [ + { fields: { title: 'Test', completed: false }, id: 1 }, + ], + pageInfo: { + totalRows: 1, + page: 1, + pageSize: 25, + isFirstPage: true, + isLastPage: true + }, + nestedNext: null, + nestedPrev: null, + next: null, + prev: null + }; + mockFetch.mockResolvedValue({ + ok: true, + statusText: "OK", + json: async () => mockResponse, + }); + + const result = await api.fetch({ + action: 'LIST', + query: { + fields: ['title', 'completed'], + sort: { field: 'title', direction: 'asc' } + } + }); + + const expectedUrl = `${origin}/api/v3/data/${baseId}/${tableId}/records`; + const calledUrl = mockFetch.mock.calls[0][0]; + const calledOptions = mockFetch.mock.calls[0][1]; + + expect(calledUrl).toContain(expectedUrl); + expect(calledUrl).toContain('sort=%7B%22direction%22%3A%22asc%22%2C%22field%22%3A%22title%22%7D'); // URL encoded JSON + + expect(calledOptions).toEqual(expect.objectContaining({ + method: 'get', + headers: expect.any(Headers), + })); + + + const { pageInfo, ...expectedResult } = mockResponse; + expect(result).toEqual(expectedResult); + }); + + it('should perform COUNT action', async () => { + const mockResponse = { count: 42 }; + mockFetch.mockResolvedValue({ + ok: true, + statusText: "OK", + json: async () => mockResponse, + }); + + const result = await api.fetch({ action: 'COUNT' }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining(`/api/v3/data/${baseId}/${tableId}/records`), + expect.objectContaining({ method: 'get' }) + ); + expect(result).toEqual(mockResponse); + }); + + it('should perform CREATE action', async () => { + const newRecord = { title: 'New Task', completed: false }; + const mockResponse = { + records: [{ fields: newRecord, id: "123" }] + }; + + mockFetch.mockResolvedValue({ + ok: true, + statusText: "OK", + json: async () => mockResponse, + }); + + const result = await api.fetch({ + action: 'CREATE', + body: { fields: newRecord } + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining(`/api/v3/data/${baseId}/${tableId}/records`), + expect.objectContaining({ + method: 'post', + body: JSON.stringify({ fields: newRecord }), + }) + ); + expect(result).toEqual(mockResponse); + }); + + it('should perform DELETE action', async () => { + mockFetch.mockResolvedValue({ + ok: true, + statusText: "OK", + json: async () => ({}), + }); + + await api.fetch({ + action: 'DELETE', + body: { id: 123 } + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining(`/api/v3/data/${baseId}/${tableId}/records`), + expect.objectContaining({ + method: 'patch', + body: JSON.stringify({ id: 123 }), + }) + ); + }); + + it('should perform UPDATE action', async () => { + const updateData = { fields: { title: 'Updated', completed: true }, id: "123" }; + mockFetch.mockResolvedValue({ + ok: true, + statusText: "OK", + json: async () => ({}), + }); + + await api.fetch({ + action: 'UPDATE', + body: updateData + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining(`/api/v3/data/${baseId}/${tableId}/records`), + expect.objectContaining({ + method: 'patch', + body: JSON.stringify(updateData), + }) + ); + }); +}); \ No newline at end of file From c35d4bff940fe218023ab96faaae2359a6a5aa6e Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:42:15 -0500 Subject: [PATCH 12/13] chore: update test file for typed-nocodb-api to use new schema and fix formatting --- core/typed-nocodb-api/test/index.test.ts | 370 ++++++++++++----------- 1 file changed, 192 insertions(+), 178 deletions(-) diff --git a/core/typed-nocodb-api/test/index.test.ts b/core/typed-nocodb-api/test/index.test.ts index 78be9406..d33cd5ae 100644 --- a/core/typed-nocodb-api/test/index.test.ts +++ b/core/typed-nocodb-api/test/index.test.ts @@ -1,181 +1,195 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { createApi } from '../src/index'; -import * as z from 'zod'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as z from "zod"; -const schema = z.object({ - title: z.string(), - completed: z.boolean(), +import { createApi } from "../src/index"; + +const apiSchema = z.object({ + completed: z.boolean(), + title: z.string(), }); -describe('typed-nocodb-api', () => { - const baseId = 'baseId'; - const origin = 'http://localhost:8080'; - const tableId = 'tableId'; - const token = 'test-token'; - - const api = createApi({ - baseId, - origin, - schema, - tableId, - token, - }); - - const mockFetch = vi.fn(); - - beforeEach(() => { - vi.stubGlobal('fetch', mockFetch); - mockFetch.mockReset(); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - }); - - it('should throw if no token is provided', async () => { - const apiNoToken = createApi({ - baseId, - origin, - schema, - tableId, - }); - - // @ts-expect-error - testing runtime check - await expect(apiNoToken.fetch({ action: 'LIST' })).rejects.toThrow('no token provided'); - }); - - it('should perform LIST action', async () => { - const mockResponse = { - records: [ - { fields: { title: 'Test', completed: false }, id: 1 }, - ], - pageInfo: { - totalRows: 1, - page: 1, - pageSize: 25, - isFirstPage: true, - isLastPage: true - }, - nestedNext: null, - nestedPrev: null, - next: null, - prev: null - }; - mockFetch.mockResolvedValue({ - ok: true, - statusText: "OK", - json: async () => mockResponse, - }); - - const result = await api.fetch({ - action: 'LIST', - query: { - fields: ['title', 'completed'], - sort: { field: 'title', direction: 'asc' } - } - }); - - const expectedUrl = `${origin}/api/v3/data/${baseId}/${tableId}/records`; - const calledUrl = mockFetch.mock.calls[0][0]; - const calledOptions = mockFetch.mock.calls[0][1]; - - expect(calledUrl).toContain(expectedUrl); - expect(calledUrl).toContain('sort=%7B%22direction%22%3A%22asc%22%2C%22field%22%3A%22title%22%7D'); // URL encoded JSON - - expect(calledOptions).toEqual(expect.objectContaining({ - method: 'get', - headers: expect.any(Headers), - })); - - - const { pageInfo, ...expectedResult } = mockResponse; - expect(result).toEqual(expectedResult); - }); - - it('should perform COUNT action', async () => { - const mockResponse = { count: 42 }; - mockFetch.mockResolvedValue({ - ok: true, - statusText: "OK", - json: async () => mockResponse, - }); - - const result = await api.fetch({ action: 'COUNT' }); - - expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining(`/api/v3/data/${baseId}/${tableId}/records`), - expect.objectContaining({ method: 'get' }) - ); - expect(result).toEqual(mockResponse); - }); - - it('should perform CREATE action', async () => { - const newRecord = { title: 'New Task', completed: false }; - const mockResponse = { - records: [{ fields: newRecord, id: "123" }] - }; - - mockFetch.mockResolvedValue({ - ok: true, - statusText: "OK", - json: async () => mockResponse, - }); - - const result = await api.fetch({ - action: 'CREATE', - body: { fields: newRecord } - }); - - expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining(`/api/v3/data/${baseId}/${tableId}/records`), - expect.objectContaining({ - method: 'post', - body: JSON.stringify({ fields: newRecord }), - }) - ); - expect(result).toEqual(mockResponse); - }); - - it('should perform DELETE action', async () => { - mockFetch.mockResolvedValue({ - ok: true, - statusText: "OK", - json: async () => ({}), - }); - - await api.fetch({ - action: 'DELETE', - body: { id: 123 } - }); - - expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining(`/api/v3/data/${baseId}/${tableId}/records`), - expect.objectContaining({ - method: 'patch', - body: JSON.stringify({ id: 123 }), - }) - ); - }); - - it('should perform UPDATE action', async () => { - const updateData = { fields: { title: 'Updated', completed: true }, id: "123" }; - mockFetch.mockResolvedValue({ - ok: true, - statusText: "OK", - json: async () => ({}), - }); - - await api.fetch({ - action: 'UPDATE', - body: updateData - }); - - expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining(`/api/v3/data/${baseId}/${tableId}/records`), - expect.objectContaining({ - method: 'patch', - body: JSON.stringify(updateData), - }) - ); - }); -}); \ No newline at end of file +describe("typed-nocodb-api", () => { + const baseId = "baseId"; + const origin = "http://localhost:8080"; + const tableId = "tableId"; + const token = "test-token"; + + const api = createApi({ + baseId, + origin, + schema: apiSchema, + tableId, + token, + }); + + const mockFetch = vi.fn(); + + beforeEach(() => { + vi.stubGlobal("fetch", mockFetch); + mockFetch.mockReset(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("should throw if no token is provided", async () => { + const apiNoToken = createApi({ + baseId, + origin, + schema: apiSchema, + tableId, + }); + + await expect(apiNoToken.fetch({ action: "LIST" })).rejects.toThrow( + "no token provided", + ); + }); + + it("should perform LIST action", async () => { + const mockResponse = { + nestedNext: null, + nestedPrev: null, + next: null, + pageInfo: { + isFirstPage: true, + isLastPage: true, + page: 1, + pageSize: 25, + totalRows: 1, + }, + prev: null, + records: [{ fields: { completed: false, title: "Test" }, id: 1 }], + }; + mockFetch.mockResolvedValue({ + json: async () => mockResponse, + ok: true, + statusText: "OK", + }); + + const result = await api.fetch({ + action: "LIST", + query: { + fields: ["title", "completed"], + sort: { direction: "asc", field: "title" }, + }, + }); + + const expectedUrl = `${origin}/api/v3/data/${baseId}/${tableId}/records`; + const calledUrl = mockFetch.mock.calls[0][0]; + const calledOptions = mockFetch.mock.calls[0][1]; + + expect(calledUrl).toContain(expectedUrl); + expect(calledUrl).toContain( + "sort=%7B%22direction%22%3A%22asc%22%2C%22field%22%3A%22title%22%7D", + ); // URL encoded JSON + + expect(calledOptions).toEqual( + expect.objectContaining({ + headers: expect.any(Headers), + method: "get", + }), + ); + + const { pageInfo, ...expectedResult } = mockResponse; + expect(result).toEqual(expectedResult); + }); + + it("should perform COUNT action", async () => { + const mockResponse = { count: 42 }; + mockFetch.mockResolvedValue({ + json: async () => mockResponse, + ok: true, + statusText: "OK", + }); + + const result = await api.fetch({ action: "COUNT" }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining( + `/api/v3/data/${baseId}/${tableId}/records`, + ), + expect.objectContaining({ method: "get" }), + ); + expect(result).toEqual(mockResponse); + }); + + it("should perform CREATE action", async () => { + const newRecord = { completed: false, title: "New Task" }; + const mockResponse = { + records: [{ fields: newRecord, id: "123" }], + }; + + mockFetch.mockResolvedValue({ + json: async () => mockResponse, + ok: true, + statusText: "OK", + }); + + const result = await api.fetch({ + action: "CREATE", + body: { fields: newRecord }, + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining( + `/api/v3/data/${baseId}/${tableId}/records`, + ), + expect.objectContaining({ + body: JSON.stringify({ fields: newRecord }), + method: "post", + }), + ); + expect(result).toEqual(mockResponse); + }); + + it("should perform DELETE action", async () => { + mockFetch.mockResolvedValue({ + json: async () => ({}), + ok: true, + statusText: "OK", + }); + + await api.fetch({ + action: "DELETE", + body: { id: 123 }, + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining( + `/api/v3/data/${baseId}/${tableId}/records`, + ), + expect.objectContaining({ + body: JSON.stringify({ id: 123 }), + method: "patch", + }), + ); + }); + + it("should perform UPDATE action", async () => { + const updateData = { + fields: { completed: true, title: "Updated" }, + id: "123", + }; + mockFetch.mockResolvedValue({ + json: async () => ({}), + ok: true, + statusText: "OK", + }); + + await api.fetch({ + action: "UPDATE", + body: updateData, + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining( + `/api/v3/data/${baseId}/${tableId}/records`, + ), + expect.objectContaining({ + body: JSON.stringify(updateData), + method: "patch", + }), + ); + }); +}); From 7bf56dcbd28adf506a6a22bba65600e287ade3b8 Mon Sep 17 00:00:00 2001 From: stephansama Date: Tue, 10 Feb 2026 08:43:51 -0500 Subject: [PATCH 13/13] chore: updated input implementation --- core/typed-nocodb-api/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/typed-nocodb-api/src/index.ts b/core/typed-nocodb-api/src/index.ts index ca77bd11..48716cb2 100644 --- a/core/typed-nocodb-api/src/index.ts +++ b/core/typed-nocodb-api/src/index.ts @@ -93,10 +93,10 @@ export function createApi({ type FetchOptions = { [A in ACTION]: ("inputSchema" extends keyof API[A] - ? { body: z.infer } + ? { body: z.input } : {}) & ("querySchema" extends keyof API[A] - ? { query?: z.infer } + ? { query?: z.input } : {}) & { action: A; token?: string;