Skip to content
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,57 @@ await deleteMany({ collectionName: 'orders', filter: '{}', allRecords: true });

Other gateway rules that now show up as errors from these hooks: an update body with `$` operators (`$inc`, `$set`, …) is refused with `CM-ERRORS-DATABASE-035` (send the plain fields to set); a broken insert or replace document answers `CM-ERRORS-DATABASE-036`; soft-deleted records are "not found" for update, replace and change-owner, and a bulk update skips them. A caller with only own-record rights may now use the bulk hooks; they touch only that caller's records.

### Expanded references and files by id (core SDK, schema-content)

A reference field (user, role, taxonomy term, record of another collection,
file) stores an id. Ask the three end-user reads for `expandReferences: true`
and every reference value comes back as `{ id, display }` — `display` is the
target's display field as the schema names it, `null` when the target is gone.
Nothing new to wire: the option is a field of the request the hooks already
pass through.

```tsx
const { data } = useFindCollectionQuery({
collectionName: 'articles',
pageSize: 20,
expandReferences: true,
});
// data.result → '[{"_id":"a1","author":{"id":"u1","display":"Ada"},"cover":{"id":"nbfl_42","display":"cover.png"}}]'

useFindOneQuery({ collectionName: 'articles', id: 'a1', expandReferences: true });
useFindOwnQuery({ collectionName: 'articles', expandReferences: true });
```

An expanded read needs read permission on every source the schema links to
(users, roles, taxonomy, collection, files). Without it the read is refused
with `CM-ERRORS-DATABASE-056`, naming the source, and lands in `error` like
any other refusal. The cache key stays the collection: a write to a
_referenced_ collection does not refetch the expanded read — `refetch()` when
a display value must be fresh.

A file reference's `id` is a stable file id (`nbfl_…`), not a path. Two hooks
read a file by that id, so a moved or renamed file is still found:

| Hook | SDK method | Route | Cache |
| ------------------------ | ----------------------- | ------------------------------------------------------ | ---------------- |
| `useGetFileByIdQuery` | `hub.files.getFileById` | `GET /{version}/files/item/by-id` | provides `Files` |
| `useGetFileByIdApiQuery` | `api.files.getFileById` | `GET /{version}/files/{filesIntegrationId}/by-id/{id}` | provides `Files` |

```tsx
const { data } = useGetFileByIdApiQuery({ filesIntegrationId: 'nbin_1', id: 'nbfl_42' });
// data.file (resource, path), data.isPublic, data.publicUrl
```

The Api hook carries the `Api` suffix for the same reason `useDeleteFileApiMutation`
does: both surfaces share one flat endpoint map. Both answers include
`isPublic` / `publicUrl`, and publishing, unpublishing or deleting a file
invalidates `Files`, so they refetch exactly then.

Nested documents and arrays are plain JSON in `document` / `update`. An update
addresses a nested path (`{"$set":{"address.city":"Vilnius"}}`) or an array
element by filter — `items.$[it].qty` with `arrayFilters: '[{"it.sku":"A1"}]'`
(a JSON string) on `useUpdateOneMutation` / `useUpdateManyMutation`.

### Schemas per environment

`useGetDatabaseSchemasQuery` returns only the schemas of the environment the request targets: the `env` argument, or else the client's environment (`norbix.setEnvironment(...)`, sent as the `norbix-env` header), or `PROD` when neither is set. Each row carries `env`. The schema list settings (`useGetDatabaseSchemaListSettingsQuery` / `useUpdateDatabaseSchemaListSettingsMutation`) are stored per environment too.
Expand Down
50 changes: 50 additions & 0 deletions docs/tasks/schema-content-react-redux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Schema content — react-redux hooks for expanded references and files by id

## Goal

Give `@norbix.ai/react-redux` the hooks the schema-content campaign adds to the core SDK (`@norbix.ai/ts` branch `audit/schema-content`): `expandReferences` on the three end-user record reads, and the two "file by id" reads.

Not in scope: merging — this branch ships to `main` only after the gateway campaign lands and `@norbix.ai/ts` releases; the generated DTO changes (ObjectFieldDto, ArrayFieldDto, JsonFieldDto, CurrencyDefaultDto, the new field properties, the new error codes) — this package carries no DTO of its own, it reads them from the core SDK's types; the `norbix.collection()` helper (plain SDK, no hook).

## Plan

1. chore(deps): build against the core SDK branch — done. `npm pack` in `/Users/djovaisas/Projects/norbix/worktrees/norbix-js/audit/schema-content` → `norbix.ai-ts-1.2.0.tgz` (scratch folder), then `npm install --no-save <tarball>` in this worktree. `package.json` / `package-lock.json` untouched: the tarball is a local overlay, not a dependency anyone can install. See "Needs you".
2. feat(files): `useGetFileByIdQuery` (`hub.files.getFileById`, `GET /{version}/files/item/by-id`) and `useGetFileByIdApiQuery` (`api.files.getFileById`, `GET /{version}/files/{filesIntegrationId}/by-id/{id}`) — done, `src/hooks/hub/files.ts`, `src/hooks/api/files.ts`.
3. docs(database): `expandReferences` and `arrayFilters` on the existing record hooks — done, `src/hooks/api/database.ts` (hook comments; no new hook: the three find hooks already pass the whole request through, the option is a request field).
4. test(files, database): hook-shape tests, fake-transport tests and real-SDK fake-fetch wire tests — done, `tests/files.test.ts`, `tests/database.test.ts`.
5. docs(readme): "Expanded references and files by id" section — done, `README.md`.
6. ship: `nbx-ship --no-merge` — done, pull request link in "Needs you".
7. build(deps): `@norbix.ai/ts` `^4.14.0` (dev) / `>=4.14.0` (peer) — done, `package.json`, `package-lock.json` (sdk-ts #79 released as v4.14.0 on 2026-10-07; lockfile also moves `source-map-js` 1.2.1 → 1.2.2 and the bundled `npm` 11.19.1 → 11.21.0 for the OSV scan).
8. merge: "Rebase and merge" once the Security scan is green — todo, see "Needs you".

## Changes

| file | what changed | plan step # |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `src/hooks/api/files.ts` | `getFileByIdApi` query (provides `Files`); header route list and count 10 → 11 | 2 |
| `src/hooks/hub/files.ts` | `getFileById` query (provides `Files`); header route list and count 22 → 23 | 2 |
| `src/hooks/api/database.ts` | comments on `findCollection` / `findOne` / `findOwn` (`expandReferences`, error 056) and `updateOne` / `updateMany` (`arrayFilters`) | 3 |
| `tests/files.test.ts` | counts 23 / 11; both by-id hooks reach their SDK method and are `Files` queries; not shadowed in the flat map; real-SDK fake-fetch: route with the ids substituted, `isPublic` / `publicUrl` parsed | 4 |
| `tests/database.test.ts` | real-SDK fake-fetch: `expandReferences=true` reaches the wire on `findCollection`, `findOne`, `findOwn` (Api), `arrayFilters` on `updateOne`; a 056 refusal lands as the serialized error | 4 |
| `README.md` | section "Expanded references and files by id (core SDK, schema-content)" | 5 |
| `docs/tasks/schema-content-react-redux.md` | this file | — |

## Findings

- The hub `getFileById` request DTO (`CodeMashHub2.GetFileById`) carries `filesIntegrationId` and `id` as plain `@ApiMember`s with no path params, so the core SDK sends them as query string on `GET /v2/files/item/by-id`; the Api one substitutes both into the path. The wire tests pin both. Nothing to fix here.
- `apiDatabase` hooks still pass `(arg as { collectionName?: string })` for the tag id — the arg type already has `collectionName`, the cast is a leftover from before the typed `Arg<>` helper. Left as is (not this task).
- This repo's commitlint (`husky` commit-msg hook, `@commitlint/config-conventional`) refuses a `[N]` step prefix on the subject, so the commits here are plain conventional subjects with `Step N of docs/tasks/…` in the body. Same as every earlier task in this repo.
- `npx prettier --check .` reports the same 20 pre-existing files as the last task (docs, examples, `tests/provider.test.tsx`). Not touched.

## Rejected / moved out

- Bumping `@norbix.ai/ts` in `package.json` to the version carrying these methods — impossible now: that version is not published (the core SDK branch is also waiting for the gateway campaign). The bump is the first commit on this branch once `@norbix.ai/ts` releases; until then the pull request checks run against npm's 4.11.0 and the typecheck / tests are red there by design (see "Needs you").
- A per-id cache tag for the by-id reads (like `getPublicFile`'s `Files/PUBLIC:<id>`) — not done: `isPublic` / `publicUrl` are part of the by-id answer, and publish / unpublish / delete invalidate the plain `Files` tag, so the whole-tag refetch is the correct one; a private id tag would make those answers stale.

## Needs you

- Pull request https://github.com/norbix-code/react-redux/pull/43: the `@norbix.ai/ts` bump to 4.14.0 is on the branch (plan step 7; lint, typecheck, 269 tests, build green locally). One check stays red and it is not the SDK: **Security scan → OSV** flags `postcss-selector-parser` 7.1.4 (GHSA-rj75-hqrm-r3gf, dev-only, bundled inside the `npm` CLI that `@semantic-release/npm` runs). No update fixes it — npm 11.21.0, the newest in the range semantic-release accepts, still bundles 7.1.4, and overrides cannot change bundled packages. The fix is a dated `[[IgnoredVulns]]` entry in `osv-scanner.toml` like the three `brace-expansion` ones (pull request 34). The agent did not add it (an ignore entry is your call). Add it (`id = "GHSA-rj75-hqrm-r3gf"`, `ignoreUntil = 2026-11-07`, dev-only reason), push, then `nbx-ship --wait-release --cleanup` from this worktree.

## Open questions

- None.
58 changes: 29 additions & 29 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"@norbix.ai/ts": ">=4.11.0",
"@norbix.ai/ts": ">=4.14.0",
"@reduxjs/toolkit": ">=2.0.0",
"react": ">=18.0.0",
"react-redux": ">=9.0.0"
Expand All @@ -68,7 +68,7 @@
"@commitlint/cli": "^21.2.2",
"@commitlint/config-conventional": "^21.2.2",
"@eslint/js": "^10.0.1",
"@norbix.ai/ts": "^4.11.0",
"@norbix.ai/ts": "^4.14.0",
"@reduxjs/toolkit": "^2.12.0",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/github": "^12.0.9",
Expand Down
24 changes: 24 additions & 0 deletions src/hooks/api/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ type GetDatabaseSchemas = Norbix['api']['database']['getDatabaseSchemas'];
*/
export const apiDatabase = (b: Builder) => ({
// ---- Collections — Read ----
/**
* `expandReferences: true` on this request (and on `findOne` / `findOwn`)
* makes every reference value come back as `{ id, display }` — `display`
* is the target's `displayField` per the schema, `null` when the target is
* gone — instead of the stored id. It needs read permission on every
* source the schema links to (users, roles, taxonomy, collection, files);
* otherwise the read is refused with `CM-ERRORS-DATABASE-056` naming the
* source, which lands in `error` like any other gateway refusal.
*
* The cache key is still the collection: an expanded read and a plain read
* of the same collection are two entries under one tag, and a write to a
* referenced collection does NOT refetch the expanded one — `refetch()`
* when a display value must be fresh. A file reference's `id` is what
* `useGetFileByIdApiQuery` reads.
*/
findCollection: b.query<Result<Find>, Arg<Find>>({
query: (args) => (norbix) => norbix.api.database.find(args),
providesTags: (_res, _err, arg) => [
Expand All @@ -47,6 +62,7 @@ export const apiDatabase = (b: Builder) => ({
],
}),

// Same `expandReferences` option as `findCollection`.
findOne: b.query<Result<FindOne>, Arg<FindOne>>({
query: (args) => (norbix) => norbix.api.database.findOne(args),
providesTags: (_res, _err, arg) => [
Expand All @@ -58,6 +74,7 @@ export const apiDatabase = (b: Builder) => ({
}),

// Only the records the caller is responsible for (owner-scoped find).
// Same `expandReferences` option as `findCollection`.
findOwn: b.query<Result<FindOwn>, Arg<FindOwn>>({
query: (args) => (norbix) => norbix.api.database.findOwn(args),
providesTags: (_res, _err, arg) => [
Expand Down Expand Up @@ -117,6 +134,12 @@ export const apiDatabase = (b: Builder) => ({
],
}),

/**
* `update` may address nested paths (`{"$set":{"address.city":"Vilnius"}}`)
* and array elements by position (`items.0.qty`) or by filter
* (`items.$[it].qty` + `arrayFilters: '[{"it.sku":"A1"}]'` — a JSON string,
* same as on `updateMany`).
*/
updateOne: b.mutation<Result<UpdateOne>, Arg<UpdateOne>>({
query: (args) => (norbix) => norbix.api.database.updateOne(args),
invalidatesTags: (_res, _err, arg) => [
Expand All @@ -127,6 +150,7 @@ export const apiDatabase = (b: Builder) => ({
],
}),

// Takes the same `arrayFilters` JSON string as `updateOne`.
updateMany: b.mutation<Result<UpdateMany>, Arg<UpdateMany>>({
query: (args) => (norbix) => norbix.api.database.updateMany(args),
invalidatesTags: (_res, _err, arg) => [
Expand Down
Loading