Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .config/.cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"macchiato",
"manypkg",
"multipublish",
"nocodb",
"nodemon",
"nvim",
"nvmrc",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Package description is vague.

The description "zod nocodb api" is noticeably less informative than other workspace entries. Consider aligning it with the issue objective, e.g., "Typed API client for NocoDB using Zod". This description likely comes from the package's package.json description field and would need to be updated there as the source of truth.

🤖 Prompt for AI Agents
In `@README.md` at line 62, The README entry for the workspace package
[typed-nocodb-api] uses the vague description "zod nocodb api"; update the
package's source-of-truth description in the package.json for
`@stephansama/typed-nocodb-api` to a clearer phrase such as "Typed API client for
NocoDB using Zod" (or similar), then regenerate or update the README table so
the README row for typed-nocodb-api reflects that new description; locate
package.json in the core/typed-nocodb-api module and the README table row that
references typed-nocodb-api to ensure both are consistent.

| [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 |

<!-- WORKSPACE end -->
Expand Down
28 changes: 28 additions & 0 deletions core/typed-nocodb-api/README.md
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hyphenate compound modifier: "standard-schema-compatible".

Per grammar convention, compound adjectives before a noun should be hyphenated.

Proposed fix
-standard schema compatible nocodb api
+standard-schema-compatible NocoDB API
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
standard schema compatible nocodb api
standard-schema-compatible NocoDB API
🧰 Tools
🪛 LanguageTool

[grammar] ~8-~8: Use a hyphen to join words.
Context: ...nsama/typed-nocodb-api) standard schema compatible nocodb api ##### Table of co...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/README.md` at line 8, Update the README header phrase
"standard schema compatible nocodb api" to use a hyphenated compound adjective
and proper capitalization (e.g., "standard-schema-compatible NocoDB API") so the
compound modifier before the noun is grammatically correct; locate and replace
the exact string "standard schema compatible nocodb api" in the README.md
content with the hyphenated/capitalized version.


##### Table of contents

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Heading level skips from h1 to h5 — violates markdownlint MD001.

Heading levels should increment by one. Use ## instead of ##### for the "Table of contents" heading.

Proposed fix
-##### Table of contents
+## Table of contents
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 10-10: Heading levels should only increment by one level at a time
Expected: h2; Actual: h5

(MD001, heading-increment)

🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/README.md` at line 10, The "Table of contents" heading
in README.md uses a level-5 heading (#####) which skips from the document H1 and
violates markdownlint MD001; update the heading to the correct incremental level
(use ## Table of contents) so headings increment by one and comply with MD001,
ensuring the visible text "Table of contents" remains unchanged.


<details><summary>Open Table of contents</summary>

- [Installation](#installation)
- [Usage](#usage)

</details>

## Installation

```sh
pnpm install @stephansama/typed-nocodb-api
```

## Usage

> \[!CAUTION]
> WIP
27 changes: 27 additions & 0 deletions core/typed-nocodb-api/example/index.js
Original file line number Diff line number Diff line change
@@ -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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Importing from ../dist/index.cjs is fragile.

This requires the package to be pre-built and couples the example to a specific build output filename. Consider importing from the package name (@stephansama/typed-nocodb-api) instead, which would resolve via the exports field in package.json and work regardless of the build tool's output format.

🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/example/index.js` at line 3, The example imports
directly from a build artifact ("../dist/index.cjs"), which is fragile; update
the import to use the package entry resolved via package.json exports by
replacing the build-artifact import with an import from the package name
"@stephansama/typed-nocodb-api" so the example works whether or not the project
is pre-built and regardless of output filename; adjust any relative import
statements in core/typed-nocodb-api/example/index.js to import from the package
name instead.


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,
});
Comment on lines +5 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

process.env values may be undefined where string is required.

createApi expects baseId: string and tableId: string, but process.env.NOCODB_BASE and process.env.NOCODB_TABLE are string | undefined. This will cause a type error in strict TS (though this is a .js file, the runtime behavior with undefined could produce malformed API URLs like /api/v3/data/undefined/undefined/records).

Consider adding guards or defaults:

🛡️ Proposed fix
+const baseId = process.env.NOCODB_BASE;
+const tableId = process.env.NOCODB_TABLE;
+const token = process.env.NOCODB_TOKEN;
+
+if (!baseId || !tableId || !token) {
+	throw new Error("NOCODB_BASE, NOCODB_TABLE, and NOCODB_TOKEN environment variables are required");
+}
+
 const api = createApi({
-	baseId: process.env.NOCODB_BASE,
+	baseId,
 	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,
+	tableId,
+	token,
 });
🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/example/index.js` around lines 5 - 16, The createApi
call uses baseId and tableId from process.env (process.env.NOCODB_BASE,
process.env.NOCODB_TABLE) which can be undefined; add a guard before calling
createApi to validate those env vars and either throw a clear error or supply
safe defaults so createApi always receives strings; update the initialization
around createApi to check/normalize baseId and tableId (and optionally
NOCODB_TOKEN) and fail fast with a descriptive message if missing.


export function callApi() {
api.fetch({
action: "LIST",
});
}
Comment on lines +18 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

api.fetch is async but neither awaited nor returned.

The promise from api.fetch is fire-and-forget here — errors will be unhandled rejections and the caller gets no result. Even for an example, this teaches incorrect usage.

♻️ Proposed fix
-export function callApi() {
-	api.fetch({
+export async function callApi() {
+	return api.fetch({
 		action: "LIST",
 	});
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function callApi() {
api.fetch({
action: "LIST",
});
}
export async function callApi() {
return api.fetch({
action: "LIST",
});
}
🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/example/index.js` around lines 18 - 22, The callApi
function currently calls the async api.fetch without awaiting or returning its
promise, causing unhandled rejections and no result propagation; fix by making
callApi async (add async to its declaration) and either await the call (const
res = await api.fetch(...); return res;) or directly return the promise (return
api.fetch(...);) so callers receive the result and errors propagate correctly —
locate the callApi function and the api.fetch invocation to apply this change.

49 changes: 49 additions & 0 deletions core/typed-nocodb-api/package.json
Original file line number Diff line number Diff line change
@@ -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": {},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Empty dependencies object is unnecessary.

If there are no runtime dependencies, omit the field entirely to reduce noise.

Proposed fix
-	"dependencies": {},
 	"devDependencies": {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"dependencies": {},
"devDependencies": {
🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/package.json` at line 26, The package.json currently
contains an empty "dependencies" object which is unnecessary; remove the
"dependencies" key entirely from core/typed-nocodb-api's package.json so the
manifest only includes fields that have values (leaving devDependencies,
scripts, name, version, etc. intact), ensuring the file remains valid JSON after
deletion.

"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"
}
}
141 changes: 141 additions & 0 deletions core/typed-nocodb-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<Schema extends z.ZodObject>({
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`,
Comment on lines +36 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent id type across actions: z.string() vs z.number().

  • CREATE response uses id: z.string() (Line 40)
  • DELETE input uses id: z.number() (Line 45)
  • LIST response uses id: z.number() (Line 66)
  • READ response uses id: z.number() (Line 72)
  • UPDATE input uses id: z.string() (Line 76)

The id field is z.string() in CREATE/UPDATE but z.number() in DELETE/LIST/READ. Verify which type NocoDB v3 actually returns and unify.

Also applies to: 44-49, 66-66, 70-73, 75-79

🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/src/index.ts` around lines 36 - 42, The Zod schema for
the record id is inconsistent across the action definitions (CREATE, DELETE,
LIST, READ, UPDATE) in index.ts; determine the actual id type returned by NocoDB
v3 (string or number) and update all occurrences so they match: e.g., the
CREATE.responseSchema records' id, UPDATE inputSchema id, DELETE inputSchema id,
LIST.responseSchema records' id, and READ.responseSchema id should all use the
same zod type (z.string() or z.number()) and related array/object wrappers;
ensure you change the referenced symbols CREATE.responseSchema,
UPDATE.inputSchema, DELETE.inputSchema, LIST.responseSchema, and
READ.responseSchema to the unified type.

},
DELETE: {
inputSchema: z.object({ id: z.number() }),
method: "patch",
responseSchema: z.object(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

z.object() called without arguments — likely a runtime error.

z.object() requires a shape argument. These should be z.object({}) to represent an empty object schema.

Proposed fix
-			responseSchema: z.object(),
+			responseSchema: z.object({}),

Apply to both Line 47 (DELETE) and Line 78 (UPDATE).

Also applies to: 78-78

🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/src/index.ts` at line 47, The schema definition uses
z.object() with no arguments which will throw at runtime; update the
responseSchema occurrences to use an explicit empty shape by replacing
z.object() with z.object({}) in both places where responseSchema is defined (the
DELETE responseSchema and the UPDATE responseSchema) so the Zod schema
represents an empty object rather than calling z.object with no params.

url: `/api/v3/data/${baseId}/${tableId}/records`,
},
Comment on lines +44 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

DELETE action uses HTTP method "patch" instead of "delete".

This is almost certainly a copy-paste error from UPDATE. The NocoDB v3 delete endpoint expects the DELETE HTTP method.

Proposed fix
 		DELETE: {
 			inputSchema: z.object({ id: z.number() }),
-			method: "patch",
+			method: "delete",
 			responseSchema: z.object(),
 			url: `/api/v3/data/${baseId}/${tableId}/records`,
 		},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DELETE: {
inputSchema: z.object({ id: z.number() }),
method: "patch",
responseSchema: z.object(),
url: `/api/v3/data/${baseId}/${tableId}/records`,
},
DELETE: {
inputSchema: z.object({ id: z.number() }),
method: "delete",
responseSchema: z.object(),
url: `/api/v3/data/${baseId}/${tableId}/records`,
},
🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/src/index.ts` around lines 44 - 49, The DELETE action
in the exported routes incorrectly sets method: "patch" (likely copy-paste from
UPDATE); update the DELETE route definition in index.ts so the DELETE entry
(with inputSchema: z.object({ id: z.number() }), responseSchema, and url:
`/api/v3/data/${baseId}/${tableId}/records`) uses method: "delete" instead of
"patch" to match the NocoDB v3 delete endpoint.

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)),
}),
Comment on lines +50 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

LIST querySchema requires both fields and sort — should these be optional?

Both fields and sort are required in the query schema. Callers must always provide both when performing a LIST action. Typical REST list endpoints allow these to be optional. If NocoDB doesn't require them, mark them as optional:

Proposed fix
 		LIST: {
 			method: "get",
 			querySchema: z.object({
-				fields: z.array(z.string()).or(z.string()),
-				sort: z
+				fields: z.array(z.string()).or(z.string()).optional(),
+				sort: z
 					.object({
 						direction: z.enum(["asc", "desc"]),
 						field: z.string(),
 					})
-					.transform((input) => JSON.stringify(input)),
+					.transform((input) => JSON.stringify(input))
+					.optional(),
 			}),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)),
}),
LIST: {
method: "get",
querySchema: z.object({
fields: z.array(z.string()).or(z.string()).optional(),
sort: z
.object({
direction: z.enum(["asc", "desc"]),
field: z.string(),
})
.transform((input) => JSON.stringify(input))
.optional(),
}),
🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/src/index.ts` around lines 50 - 60, The LIST action's
querySchema currently requires both fields and sort, forcing callers to always
provide them; update LIST.querySchema so fields and sort are optional by
applying .optional() to the existing fields schema
(z.array(z.string()).or(z.string())) and to the sort schema (the
z.object({...}).transform(...)). This preserves the existing types/transform but
allows callers to omit fields and/or sort when making LIST requests.

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}`,
Comment on lines +70 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

READ action URL contains {recordId} placeholder that is never substituted.

The URL /api/v3/data/${baseId}/${tableId}/records/{recordId} contains a literal {recordId} string. The READ action has no inputSchema or parameter to accept a record ID, so the placeholder is never replaced. This will always request the wrong endpoint.

Consider adding an input for the record ID and interpolating it into the URL at fetch time:

Proposed approach

Add an inputSchema for READ and replace the placeholder in the fetch method:

 		READ: {
+			inputSchema: z.object({ id: z.number() }),
 			method: "get",
 			responseSchema: z.object({ fields: schema, id: z.number() }),
-			url: `/api/v3/data/${baseId}/${tableId}/records/{recordId}`,
+			url: `/api/v3/data/${baseId}/${tableId}/records`,
 		},

Then in the fetch method, for READ, append the ID to the URL (e.g., url + "/" + id). This requires some refactoring of the URL construction logic to handle path parameters.

🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/src/index.ts` around lines 70 - 73, The READ action
currently uses a literal "{recordId}" in url
(`/api/v3/data/${baseId}/${tableId}/records/{recordId}`) but has no input to
supply that ID; add an inputSchema (e.g., inputSchema: z.object({ recordId:
z.union([z.string(), z.number()]) })) to the READ action and update the fetch
logic (the method that builds the request URL for actions) to substitute the
provided recordId into the path (or append it to the records URL) instead of
leaving the placeholder; update any URL construction / dispatch code that
handles other actions so path parameters are replaced for READ before making the
request.

},
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<API[A]["inputSchema"]> }
: {}) &
("querySchema" extends keyof API[A]
? { query?: z.infer<API[A]["querySchema"]> }
: {}) & {
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();
}
Comment on lines +120 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

URLSearchParams does not serialize array values correctly.

The querySchema for LIST allows fields to be z.array(z.string()). When an array is passed to URLSearchParams, it will be coerced via .toString() producing "a,b,c" as a single parameter. If NocoDB expects repeated keys (fields=a&fields=b) or a different encoding, this will silently produce wrong results.

🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/src/index.ts` around lines 115 - 120, The current
serialization uses URLSearchParams(parsed).toString(), which coerces arrays into
single comma-separated values; update the logic in the block that sets params
(using parsed from current.querySchema.parse(props.query)) to build the query
string by iterating parsed's entries and appending values to a URLSearchParams
instance: for array values call append for each element (to produce repeated
keys like fields=a&fields=b), for scalar values append once, then set params =
"?" + usp.toString(); ensure you convert non-string primitives to strings when
appending.


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,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const json = await response.json();
return current.responseSchema.parse(json);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
};
}
4 changes: 4 additions & 0 deletions core/typed-nocodb-api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["../../tsconfig.base.json"],
"include": ["./src/**/*"]
}
11 changes: 11 additions & 0 deletions core/typed-nocodb-api/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -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",
});
Comment on lines +3 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing skipNodeModulesBundlezod will likely be bundled into the output.

Other packages in this repo (e.g., core/ai-commit-msg/build.mjs, core/multipublish/build.mjs) include skipNodeModulesBundle: true to avoid bundling node_modules dependencies. Without this, zod (a peer dependency) will be inlined into the dist, causing bloat and potential duplicate-instance issues at runtime.

Proposed fix
 export default defineConfig({
 	attw: true,
 	dts: true,
 	entry: ["src/index.ts"],
 	exports: true,
 	format: ["esm", "cjs"],
 	publint: true,
+	skipNodeModulesBundle: true,
 	target: "esnext",
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default defineConfig({
attw: true,
dts: true,
entry: ["src/index.ts"],
exports: true,
format: ["esm", "cjs"],
publint: true,
target: "esnext",
});
export default defineConfig({
attw: true,
dts: true,
entry: ["src/index.ts"],
exports: true,
format: ["esm", "cjs"],
publint: true,
skipNodeModulesBundle: true,
target: "esnext",
});
🤖 Prompt for AI Agents
In `@core/typed-nocodb-api/tsdown.config.ts` around lines 3 - 11, The tsdown
config omits skipNodeModulesBundle which causes node_modules like zod to be
bundled; update the defineConfig call in tsdown.config.ts (the object passed to
defineConfig) to add skipNodeModulesBundle: true so external
dependencies/peerDependencies (e.g., zod) are not inlined into the dist,
matching other configs such as core/ai-commit-msg/build.mjs and
core/multipublish/build.mjs.

12 changes: 12 additions & 0 deletions core/typed-nocodb-api/typedoc.json
Original file line number Diff line number Diff line change
@@ -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}/**"
]
}
2 changes: 1 addition & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Misleading variable name after plugin swap.

The import variable is still named eslintPluginZodX but now imports from eslint-plugin-zod (not zod-x). Rename it to match the actual package.

♻️ Proposed fix
-import eslintPluginZodX from "eslint-plugin-zod";
+import eslintPluginZod from "eslint-plugin-zod";

Also update the usage on line 49:

-	eslintPluginZodX.configs.recommended,
+	eslintPluginZod.configs.recommended,
🤖 Prompt for AI Agents
In `@eslint.config.ts` at line 12, The imported variable name eslintPluginZodX is
misleading because the module is now "eslint-plugin-zod"; rename the import to
eslintPluginZod and update all usages (e.g., the plugin registration that
currently references eslintPluginZodX) to use eslintPluginZod instead so the
identifier matches the actual package name.

import { defineConfig } from "eslint/config";
import globals from "globals";
import * as jsoncParser from "jsonc-eslint-parser";
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading