Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ norbix scheduler list
| --- | --- |
| `norbix login` / `logout` / `whoami` | Authenticate and inspect the current context |
| `norbix config list/get/set/unset` | Read and change one profile in `~/.norbix/config` (`--profile`, default `[default]`) |
| `norbix db ...` | Database records (find/get/count/insert/update/replace/delete/aggregate/change-owner), schemas (create/update/publish/delete/versions/diff), schema triggers, integrations, taxonomies and terms, indexes, test-data seed — see [docs/database.md](docs/database.md) |
| `norbix db ...` | Database records (find/get/count/insert/update/replace/delete/aggregate/change-owner), schemas (create/update/publish/delete/versions/diff, and `db schema rules` — the design rules to read before writing one), schema triggers, integrations, taxonomies and terms, indexes, test-data seed — see [docs/database.md](docs/database.md) |
| `norbix files list/info/get-by-id/upload/download/sign/delete/publish/unpublish` | Upload, download, manage and publish files |
| `norbix files integrations test <id>` | Check that a files integration really works (live upload/read/list/delete probe) |
| `norbix users list/get/invite/block/unblock/delete` | Manage project users (membership) |
Expand Down
27 changes: 27 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,33 @@ norbix hub scheduler task save --body '{"name":"nightly","cron":"0 2 * * *","ini
`--field` flags); `--field:str value` forces a string when a value looks
like a number.

## 3a. Design a database schema by the rules

Before writing a data schema, print the rules and follow them:

```sh
norbix db schema rules # which field for which value, with the exact JSON shape
norbix db schema rules --json # the same as data
```

- Email → `{"type":"string","format":"email"}`; link / website / app link →
`{"type":"string","format":"uri"}` — never bare text.
- Language / locale, currency code, country, status, category — any list
people pick from → a taxonomy picker
(`{"source":"taxonomy","taxonomyId":"<id>"}`), never bare text and not an
inline enum. An enum is only for a developer-facing flag the app code
branches on.
- A person (assignee, author, `*_user_id`) → a user reference
(`{"source":"user","displayField":"displayName"}`), never a text id.
Never add `createdBy` / `owner` / `createdAt`: the platform keeps them in
`_meta` (`settings.hasRecordOwner`).

The Hub checks these rules when a schema is saved (`db schema create`,
`hub database schema save`) and when `hub account schema validate` runs. A
schema that breaks one is still saved; the answer carries `warnings` (on
stderr as text, and in the JSON on stdout). Fix each named field and save
again.

## 4. Change things in two steps

```sh
Expand Down
20 changes: 19 additions & 1 deletion docs/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ counts, the new ids, and any errors.
|---|---|---|
| `norbix db schemas [--page-size <n>] [--after <cursor>]` | list the schemas of one environment (`--env`, default PROD); each row carries `env` | `GET /schemas` |
| `norbix db schema <id>` | show one schema | `GET /schemas/{id}` |
| `norbix db schema create --name <name> --file <data-schema.json> [--ui-file <ui.json>] [--settings <json>]` | create a schema | `POST /schemas` |
| `norbix db schema rules [--json]` | print the schema design rules (which field for which value, with the exact JSON shape); sends nothing | — |
| `norbix db schema create --name <name> --file <data-schema.json> [--ui-file <ui.json>] [--settings <json>]` | create a schema; prints the Hub's design `warnings` on stderr | `POST /schemas` |
| `norbix db schema update <id> [--file <data-schema.json>] [--ui-file <ui.json>]` | save a change as the draft | `PUT /schemas/{Id}/draft` |
| `norbix db schema draft <id>` | show the unpublished draft | `GET /schemas/{Id}/draft` |
| `norbix db schema discard <id> [--yes]` | throw the draft away | `DELETE /schemas/{Id}/draft` |
Expand All @@ -163,6 +164,23 @@ counts, the new ids, and any errors.
| `norbix db schema index-status <id>` | the last schema-index run: the indexes Norbix made (`idx_<field>` per reference, `uniq_<field>` per unique field, `idx_<field>__id` for the default sort, at most 8), state `building` / `ready` / `refused` / `partial` per database | `GET /schemas/{Id}/index-status` |
| `norbix db schema delete <id> [--yes]` | delete the schema **and its records**: the collection (records and indexes) in the environment is dropped, and removed from the AI knowledge when AI embed is on; no undo. Refused while a schema trigger uses it (`CM-ERRORS-SCHEMA-017`) or a saved aggregate starts on / joins it (`CM-ERRORS-SCHEMA-018`, the aggregates are in `BlockerAggregateNames`); then nothing is dropped | `DELETE /schemas/{Id}` |

**Design rules first.** Run `norbix db schema rules` before writing
`--file`: an email is `{"type":"string","format":"email"}`, a link
`{"type":"string","format":"uri"}`; language / locale, currency code,
country and every list people pick from are a taxonomy picker
(`{"source":"taxonomy","taxonomyId":"<id>"}`, an enum only for a
developer-facing flag); a person is a user reference
(`{"source":"user","displayField":"displayName"}`). When a field breaks one of
these rules the Hub still saves the schema and answers with `warnings`, for
example:

```
Warning: the schema was saved, but 2 fields break a design rule:
- app_link: 'app_link' looks like a link: use a string with "format": "uri" (UI widget website), not bare text.
- language: 'language' looks like a language: use a taxonomy-selection field (a language taxonomy, one term per language) so values are validated and translatable. Free text only when the value is deliberately not tied to any list.
Fix them and save again. Rules: norbix db schema rules
```

`--file` is the data schema (the JSON Schema of one record); `--ui-file` is the
UI schema the dashboard form uses. A change is a draft first: records keep the
published version until `schema publish`. `publish` asks first and sends the
Expand Down
2 changes: 2 additions & 0 deletions src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export interface DryRunValidation {
valid?: boolean
issues?: Array<{where?: string; code?: string; message?: string}>
collections?: string[]
/** Schema design warnings (bare-text email, link, language, currency, country, user id); never make it invalid. */
warnings?: string[]
/** Why the input was not checked (no bundleJson, the check failed). */
note?: string
}
Expand Down
10 changes: 9 additions & 1 deletion src/commands/db/schema/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ import {Flags} from '@oclif/core'

import {BaseCommand} from '../../../base.js'
import {fileSource, readJsonObject, readJsonText, type SchemaSettings} from '../../../lib/database.js'
import {printSchemaWarnings} from '../../../lib/schema-rules.js'

export default class DbSchemaCreate extends BaseCommand {
static description = `Create a database schema (a collection) from a JSON file

--file is the data schema (JSON Schema of one record). --ui-file is the
optional UI schema the dashboard form uses. The schema starts as a draft:
run \`db schema publish <id>\` to make the collection usable.`
run \`db schema publish <id>\` to make the collection usable.

Before you write the file, run \`db schema rules\`: emails and links need a
format, language / currency / country are taxonomy pickers, user ids are user
references. When a field breaks one of these rules the Hub still saves the
schema and answers with "warnings"; they are printed on stderr (with --json
they are also in the JSON on stdout).`

static examples = [
'<%= config.bin %> db schema create --name orders --file orders.schema.json',
Expand Down Expand Up @@ -37,6 +44,7 @@ run \`db schema publish <id>\` to make the collection usable.`
})

this.print(res)
printSchemaWarnings(res)
return res
}
}
Expand Down
21 changes: 21 additions & 0 deletions src/commands/db/schema/rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {BaseCommand} from '../../../base.js'
import {SCHEMA_NEVER, SCHEMA_RULES, schemaRulesText} from '../../../lib/schema-rules.js'

export default class DbSchemaRules extends BaseCommand {
static description = `Print the schema design rules — read them before writing a data schema

Which field for which value: emails and links need a format, language /
currency / country and every list people pick from are taxonomy pickers,
user ids are user references, money is a currency field, dates are epoch
integers. Each rule comes with the exact dataSchema JSON shape. Works
offline: nothing is sent to the Hub. \`db schema create\` prints the Hub's
warnings when a saved schema breaks one of these rules.`

static examples = ['<%= config.bin %> db schema rules', '<%= config.bin %> db schema rules --json']

async run(): Promise<unknown> {
await this.parse(DbSchemaRules)
this.print(schemaRulesText())
return {rules: SCHEMA_RULES, never: SCHEMA_NEVER}
}
}
6 changes: 5 additions & 1 deletion src/commands/db/schema/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export default class DbSchemaUpdate extends BaseCommand {

The change is saved as the schema's draft; records keep using the published
version until you run \`db schema publish <id>\`. \`db schema draft <id>\` shows
the draft, \`db schema discard <id>\` throws it away.`
the draft, \`db schema discard <id>\` throws it away.

Before you change the file, run \`db schema rules\`: emails and links need a
format, language / currency / country are taxonomy pickers, user ids are user
references.`

static examples = [
'<%= config.bin %> db schema update 66b2f0a1c3d4e5f6a7b8c9d0 --file orders.schema.json',
Expand Down
2 changes: 2 additions & 0 deletions src/lib/agent-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function recipeBlock(): string {
'- Pass `--json` to every command whose output you read: stdout is then exactly one JSON document (a result, or `{"error": {code, message, exit, fieldErrors, hint, docs}}`).',
'- Discover, do not guess: `norbix schema --json` (every command, `destructive`, `supportsDryRun`), then `norbix <command> --help`. No named command? `norbix hub <module> --json` lists the SDK methods and their fields.',
'- Before any change, run it with `--dry-run --json` (sends nothing) and show the user the request. Then re-run without `--dry-run`, adding `--yes` only for a destructive command (without it the command exits 3 and sends nothing).',
'- Designing a database schema? Run `norbix db schema rules` first: emails and links need a format (`email` / `uri`), language / currency / country and every list people pick from are taxonomy pickers (enum only for developer flags), user ids are user references, no createdBy / owner fields. Fix every `warnings` entry a schema save returns.',
'- Never print, paste or ask for API keys, passwords or tokens, and never read `~/.norbix`. Credentials come from `norbix login`, a `--profile`, or `NORBIX_*` variables the user set (CI: `NORBIX_HOST` + `NORBIX_API_KEY` + `NORBIX_PROJECT_ID`).',
'- Branch on the exit code: 0 ok · 2 usage (fix the call) · 3 needs `--yes` · 4 auth · 5 not found · 6 validation (`fieldErrors`) · 7 network · 8 server (retry with backoff) · 9 cancelled.',
'- Full contract: `docs/agent-contract.md` in the norbix-code/cli repo; `error.hint` and `error.docs` name the next step.',
Expand Down Expand Up @@ -58,6 +59,7 @@ export function claudeSkill(): string {
' - Not on norbix.ai? Add `--host <their dashboard or Hub address>` (or `--profile <name>`) to both steps.',
'2. Find the command: `norbix schema --json` lists every command with `destructive` and `supportsDryRun`; `norbix <command> --help` has a realistic example. No named command? `norbix hub <module> --json` (or `norbix api <module> --json`) lists the SDK methods with their fields; call one with plain words and `--body \'<json>\'`.',
'3. Read with `--json` and parse the one document on stdout. Never parse the text output.',
' Designing a database schema? Run `norbix db schema rules` first and follow it: emails and links need a format (`email` / `uri`), language / currency / country and every list people pick from are taxonomy pickers (enum only for developer flags), user ids are user references, no createdBy / owner fields. A schema save that breaks a rule still succeeds but returns `warnings`: fix each named field and save again.',
'4. Change in two steps: `norbix <command> ... --dry-run --json` prints the exact request and sends nothing — show it to the user. Then re-run without `--dry-run`, adding `--yes` only if the command is destructive. Never add `--yes` to a first attempt.',
'5. Branch on the exit code and read `error.hint` / `error.docs`: 0 ok · 2 usage · 3 add `--yes` after a dry run · 4 auth (sign in again as in step 1) · 5 not found (check id / `--env` / `--project`) · 6 validation (`error.fieldErrors`) · 7 network · 8 server (retry with backoff, keep `traceId`) · 9 cancelled.',
'',
Expand Down
11 changes: 11 additions & 0 deletions src/lib/namespace-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {usageError} from './cli-error.js'
import {MODULE_ALIASES, camelSplit, isDestructive, matchMethods, parseArgv, typeFields} from './dispatch.js'
import {readJsonInput} from './json.js'
import {describeFields, fieldKinds, methodInfo, type FieldKind} from './request-fields.js'
import {SCHEMA_RULES_POINTER, printSchemaWarnings, schemaWarnings} from './schema-rules.js'

/** The SDK methods whose `warnings` are schema design warnings. */
const SCHEMA_WARNING_METHODS = new Set(['saveDatabaseSchema', 'validateSchema'])

type SdkModule = Record<string, (request: Record<string, unknown>) => Promise<unknown>>

Expand Down Expand Up @@ -113,6 +117,7 @@ export abstract class NamespaceCommand extends BaseCommand {
`Add an ID as a plain value and request fields as flags:\n` +
` norbix ${this.target} ${moduleWord} ${this.wordForm(list[0], moduleName, hide)} <id> --someField value\n\n` +
`${VALUE_RULES}\n\n` +
`${moduleName === 'database' ? `${SCHEMA_RULES_POINTER}\n\n` : ''}` +
`Useful extras: --dry-run (preview the exact request), --yes (skip confirmation),\n` +
`--json (machine output), --profile/--env/--region/--project (context).`,
)
Expand Down Expand Up @@ -216,6 +221,10 @@ export abstract class NamespaceCommand extends BaseCommand {
const liveModule = (client[this.target] as unknown as Record<string, SdkModule>)[moduleName]
const res = await liveModule[method](request)
this.print(res)
// save_database_schema / validate_schema answer with design warnings
// (bare-text email, link, language, currency, country, user id). Only
// those two: IdResponse.warnings is shared by every id-returning endpoint.
if (SCHEMA_WARNING_METHODS.has(method)) printSchemaWarnings(res)
return res
}

Expand All @@ -241,6 +250,8 @@ export abstract class NamespaceCommand extends BaseCommand {
valid: Boolean(res.valid),
issues: (res.issues ?? []).map((i) => ({where: i.where, code: i.code, message: i.message})),
collections: res.collections ?? [],
// Only when the Hub sent design warnings, so a clean check keeps its shape.
...(schemaWarnings(res).length > 0 ? {warnings: schemaWarnings(res)} : {}),
}
} catch (error) {
return {checkedWith, checked: false, note: `the check failed: ${error instanceof Error ? error.message : String(error)}`}
Expand Down
Loading
Loading