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
18 changes: 8 additions & 10 deletions skills/objectstack-data/references/data-hooks.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -369,7 +369,7 @@ The sandbox is handed a **JSON snapshot** of these (built by
| `ctx.event` | string | e.g. `'afterUpdate'` — dispatch on it when one hook subscribes to several events. |
| `ctx.object` | string | The target object name. |
| `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. |
| `ctx.log` | `{ info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. |
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. |

Expand All@@ -395,6 +395,7 @@ org / user / transaction. Methods:
| `update(data, opts?)` | `api.write` | **`update({ id, ...fields })`** — put the id **inside** `data` |
| `upsert(data, opts?)` | `api.write` | `upsert({ … })` |
| `delete(opts)` | `api.write` | `delete({ where: { id } })` |
| `aggregate` · `updateMany` · `deleteMany` | read · write · write | also installed; same `where` shape |

**Query shape — the key is `where`.** It takes an object with `$`-operators, the
same DSL as the [objectstack-query](../../objectstack-query/SKILL.md) skill:
Expand DownExpand Up@@ -443,11 +444,11 @@ set of legal tokens (`HookBodyCapability`) is exactly five:

| Token | Unlocks |
|:--|:--|
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` |
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count` / `aggregate`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` / `updateMany` / `deleteMany` |
| `api.transaction` | `ctx.api.transaction(async () => { … })` — runs the callback's `ctx.api` ops in **one driver transaction** (commit on return, rollback on throw). Pair it with `api.write`. |
| `crypto.uuid` | `ctx.crypto.randomUUID()` |
| `log` | `ctx.log.info` / `warn` / `error(msg, data?)` |
| `log` | `ctx.log.debug` / `info` / `warn` / `error(msg, data?)` |

There is **no `http.fetch` capability** by design — outbound calls go through
Connector recipes so they stay auditable and replayable.
Expand DownExpand Up@@ -553,8 +554,7 @@ Register it like any hook — add it to `defineStack({ hooks: [fillPositionOnHir
capability it emits only when the runtime wired a logger into the hook context —
otherwise it is a **silent no-op**. Treat `ctx.log` as best-effort diagnostics,
not a reliable side-channel or proof a hook ran; to observe an effect, assert on
the data it writes. (And call `ctx.log.info(msg)` — `ctx.log` is an object, not
a function, so `ctx.log(msg)` is not callable.)
the data it writes.

---

Expand DownExpand Up@@ -589,10 +589,8 @@ interface HookContext {
organizationId?: string; // Active org — the single blessed name. Matches the
// `organization_id` column + `current_user.organizationId` (RLS).
// The former `tenantId` alias was removed in v16.
// There is no `roles` here: `session.roles` was declared but
// never produced, and was retired in 17.0.0. Privilege
// is judged by the security service (permissions / positions /
// posture), never by a role-name string in a hook.
// No `roles` here (retired in 17.0.0) — privilege is judged
// by the security service, never by a role name in a hook.
accessToken?: string;
isSystem?: boolean; // Elevated system context (engine self-writes).
};
Expand Down
10 changes: 5 additions & 5 deletions skills/objectstack-data/rules/datasources.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ Full field reference: `node_modules/@objectstack/spec/src/data/datasource.zod.ts
| `external` | A mature external DB ObjectStack does **not** own; DDL forbidden; boot mismatch **fails**. |
| `validate-only` | Like `external`, but a mismatch **warns** instead of failing boot. |

`external` settings are required iff `schemaMode !== 'managed'` (and forbidden otherwise).
`external` settings are **required** when `schemaMode !== 'managed'`.

## Federated (external) objects

Expand All@@ -38,10 +38,10 @@ ObjectSchema.create({
### ✅ / ❌ Column mapping (ADR-0062 D7)

- ✅ Map remote columns with **`external.columnMap`** (`remoteColumn → localField`).
- ❌ **Never set `field.columnName` on an external object.** The driver's query
pipeline ignores it for federated objects, so it is a silent dual-source trap.
`os build` / `os validate` **rejects** it with a clear error. (`field.columnName`
on **managed** objects is unaffected.)
- ❌ **`field.columnName` does not exist — on ANY object.** It was removed in the
16.x line (the SQL driver hardcodes the physical column to the field key, so a
custom name was ignored), and authoring it is a parse error everywhere, not
only on a federated object.

## Auto-connect (no `onEnable`)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
18 changes: 8 additions & 10 deletions skills/objectstack-data/references/data-hooks.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -369,7 +369,7 @@ The sandbox is handed a **JSON snapshot** of these (built by
| `ctx.event` | string | e.g. `'afterUpdate'` — dispatch on it when one hook subscribes to several events. |
| `ctx.object` | string | The target object name. |
| `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. |
| `ctx.log` | `{ info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. |
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. |

Expand All@@ -395,6 +395,7 @@ org / user / transaction. Methods:
| `update(data, opts?)` | `api.write` | **`update({ id, ...fields })`** — put the id **inside** `data` |
| `upsert(data, opts?)` | `api.write` | `upsert({ … })` |
| `delete(opts)` | `api.write` | `delete({ where: { id } })` |
| `aggregate` · `updateMany` · `deleteMany` | read · write · write | also installed; same `where` shape |

**Query shape — the key is `where`.** It takes an object with `$`-operators, the
same DSL as the [objectstack-query](../../objectstack-query/SKILL.md) skill:
Expand DownExpand Up@@ -443,11 +444,11 @@ set of legal tokens (`HookBodyCapability`) is exactly five:

| Token | Unlocks |
|:--|:--|
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` |
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count` / `aggregate`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` / `updateMany` / `deleteMany` |
| `api.transaction` | `ctx.api.transaction(async () => { … })` — runs the callback's `ctx.api` ops in **one driver transaction** (commit on return, rollback on throw). Pair it with `api.write`. |
| `crypto.uuid` | `ctx.crypto.randomUUID()` |
| `log` | `ctx.log.info` / `warn` / `error(msg, data?)` |
| `log` | `ctx.log.debug` / `info` / `warn` / `error(msg, data?)` |

There is **no `http.fetch` capability** by design — outbound calls go through
Connector recipes so they stay auditable and replayable.
Expand DownExpand Up@@ -553,8 +554,7 @@ Register it like any hook — add it to `defineStack({ hooks: [fillPositionOnHir
capability it emits only when the runtime wired a logger into the hook context —
otherwise it is a **silent no-op**. Treat `ctx.log` as best-effort diagnostics,
not a reliable side-channel or proof a hook ran; to observe an effect, assert on
the data it writes. (And call `ctx.log.info(msg)` — `ctx.log` is an object, not
a function, so `ctx.log(msg)` is not callable.)
the data it writes.

---

Expand DownExpand Up@@ -589,10 +589,8 @@ interface HookContext {
organizationId?: string; // Active org — the single blessed name. Matches the
// `organization_id` column + `current_user.organizationId` (RLS).
// The former `tenantId` alias was removed in v16.
// There is no `roles` here: `session.roles` was declared but
// never produced, and was retired in 17.0.0. Privilege
// is judged by the security service (permissions / positions /
// posture), never by a role-name string in a hook.
// No `roles` here (retired in 17.0.0) — privilege is judged
// by the security service, never by a role name in a hook.
accessToken?: string;
isSystem?: boolean; // Elevated system context (engine self-writes).
};
Expand Down
10 changes: 5 additions & 5 deletions skills/objectstack-data/rules/datasources.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ Full field reference: `node_modules/@objectstack/spec/src/data/datasource.zod.ts
| `external` | A mature external DB ObjectStack does **not** own; DDL forbidden; boot mismatch **fails**. |
| `validate-only` | Like `external`, but a mismatch **warns** instead of failing boot. |

`external` settings are required iff `schemaMode !== 'managed'` (and forbidden otherwise).
`external` settings are **required** when `schemaMode !== 'managed'`.

## Federated (external) objects

Expand All@@ -38,10 +38,10 @@ ObjectSchema.create({
### ✅ / ❌ Column mapping (ADR-0062 D7)

- ✅ Map remote columns with **`external.columnMap`** (`remoteColumn → localField`).
- ❌ **Never set `field.columnName` on an external object.** The driver's query
pipeline ignores it for federated objects, so it is a silent dual-source trap.
`os build` / `os validate` **rejects** it with a clear error. (`field.columnName`
on **managed** objects is unaffected.)
- ❌ **`field.columnName` does not exist — on ANY object.** It was removed in the
16.x line (the SQL driver hardcodes the physical column to the field key, so a
custom name was ignored), and authoring it is a parse error everywhere, not
only on a federated object.

## Auto-connect (no `onEnable`)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
18 changes: 8 additions & 10 deletions skills/objectstack-data/references/data-hooks.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -369,7 +369,7 @@ The sandbox is handed a **JSON snapshot** of these (built by
| `ctx.event` | string | e.g. `'afterUpdate'` — dispatch on it when one hook subscribes to several events. |
| `ctx.object` | string | The target object name. |
| `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. |
| `ctx.log` | `{ info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. |
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. |

Expand All@@ -395,6 +395,7 @@ org / user / transaction. Methods:
| `update(data, opts?)` | `api.write` | **`update({ id, ...fields })`** — put the id **inside** `data` |
| `upsert(data, opts?)` | `api.write` | `upsert({ … })` |
| `delete(opts)` | `api.write` | `delete({ where: { id } })` |
| `aggregate` · `updateMany` · `deleteMany` | read · write · write | also installed; same `where` shape |

**Query shape — the key is `where`.** It takes an object with `$`-operators, the
same DSL as the [objectstack-query](../../objectstack-query/SKILL.md) skill:
Expand DownExpand Up@@ -443,11 +444,11 @@ set of legal tokens (`HookBodyCapability`) is exactly five:

| Token | Unlocks |
|:--|:--|
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` |
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count` / `aggregate`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` / `updateMany` / `deleteMany` |
| `api.transaction` | `ctx.api.transaction(async () => { … })` — runs the callback's `ctx.api` ops in **one driver transaction** (commit on return, rollback on throw). Pair it with `api.write`. |
| `crypto.uuid` | `ctx.crypto.randomUUID()` |
| `log` | `ctx.log.info` / `warn` / `error(msg, data?)` |
| `log` | `ctx.log.debug` / `info` / `warn` / `error(msg, data?)` |

There is **no `http.fetch` capability** by design — outbound calls go through
Connector recipes so they stay auditable and replayable.
Expand DownExpand Up@@ -553,8 +554,7 @@ Register it like any hook — add it to `defineStack({ hooks: [fillPositionOnHir
capability it emits only when the runtime wired a logger into the hook context —
otherwise it is a **silent no-op**. Treat `ctx.log` as best-effort diagnostics,
not a reliable side-channel or proof a hook ran; to observe an effect, assert on
the data it writes. (And call `ctx.log.info(msg)` — `ctx.log` is an object, not
a function, so `ctx.log(msg)` is not callable.)
the data it writes.

---

Expand DownExpand Up@@ -589,10 +589,8 @@ interface HookContext {
organizationId?: string; // Active org — the single blessed name. Matches the
// `organization_id` column + `current_user.organizationId` (RLS).
// The former `tenantId` alias was removed in v16.
// There is no `roles` here: `session.roles` was declared but
// never produced, and was retired in 17.0.0. Privilege
// is judged by the security service (permissions / positions /
// posture), never by a role-name string in a hook.
// No `roles` here (retired in 17.0.0) — privilege is judged
// by the security service, never by a role name in a hook.
accessToken?: string;
isSystem?: boolean; // Elevated system context (engine self-writes).
};
Expand Down
10 changes: 5 additions & 5 deletions skills/objectstack-data/rules/datasources.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ Full field reference: `node_modules/@objectstack/spec/src/data/datasource.zod.ts
| `external` | A mature external DB ObjectStack does **not** own; DDL forbidden; boot mismatch **fails**. |
| `validate-only` | Like `external`, but a mismatch **warns** instead of failing boot. |

`external` settings are required iff `schemaMode !== 'managed'` (and forbidden otherwise).
`external` settings are **required** when `schemaMode !== 'managed'`.

## Federated (external) objects

Expand All@@ -38,10 +38,10 @@ ObjectSchema.create({
### ✅ / ❌ Column mapping (ADR-0062 D7)

- ✅ Map remote columns with **`external.columnMap`** (`remoteColumn → localField`).
- ❌ **Never set `field.columnName` on an external object.** The driver's query
pipeline ignores it for federated objects, so it is a silent dual-source trap.
`os build` / `os validate` **rejects** it with a clear error. (`field.columnName`
on **managed** objects is unaffected.)
- ❌ **`field.columnName` does not exist — on ANY object.** It was removed in the
16.x line (the SQL driver hardcodes the physical column to the field key, so a
custom name was ignored), and authoring it is a parse error everywhere, not
only on a federated object.

## Auto-connect (no `onEnable`)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
18 changes: 8 additions & 10 deletions skills/objectstack-data/references/data-hooks.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -369,7 +369,7 @@ The sandbox is handed a **JSON snapshot** of these (built by
| `ctx.event` | string | e.g. `'afterUpdate'` — dispatch on it when one hook subscribes to several events. |
| `ctx.object` | string | The target object name. |
| `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. |
| `ctx.log` | `{ info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. |
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. |

Expand All@@ -395,6 +395,7 @@ org / user / transaction. Methods:
| `update(data, opts?)` | `api.write` | **`update({ id, ...fields })`** — put the id **inside** `data` |
| `upsert(data, opts?)` | `api.write` | `upsert({ … })` |
| `delete(opts)` | `api.write` | `delete({ where: { id } })` |
| `aggregate` · `updateMany` · `deleteMany` | read · write · write | also installed; same `where` shape |

**Query shape — the key is `where`.** It takes an object with `$`-operators, the
same DSL as the [objectstack-query](../../objectstack-query/SKILL.md) skill:
Expand DownExpand Up@@ -443,11 +444,11 @@ set of legal tokens (`HookBodyCapability`) is exactly five:

| Token | Unlocks |
|:--|:--|
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` |
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count` / `aggregate`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` / `updateMany` / `deleteMany` |
| `api.transaction` | `ctx.api.transaction(async () => { … })` — runs the callback's `ctx.api` ops in **one driver transaction** (commit on return, rollback on throw). Pair it with `api.write`. |
| `crypto.uuid` | `ctx.crypto.randomUUID()` |
| `log` | `ctx.log.info` / `warn` / `error(msg, data?)` |
| `log` | `ctx.log.debug` / `info` / `warn` / `error(msg, data?)` |

There is **no `http.fetch` capability** by design — outbound calls go through
Connector recipes so they stay auditable and replayable.
Expand DownExpand Up@@ -553,8 +554,7 @@ Register it like any hook — add it to `defineStack({ hooks: [fillPositionOnHir
capability it emits only when the runtime wired a logger into the hook context —
otherwise it is a **silent no-op**. Treat `ctx.log` as best-effort diagnostics,
not a reliable side-channel or proof a hook ran; to observe an effect, assert on
the data it writes. (And call `ctx.log.info(msg)` — `ctx.log` is an object, not
a function, so `ctx.log(msg)` is not callable.)
the data it writes.

---

Expand DownExpand Up@@ -589,10 +589,8 @@ interface HookContext {
organizationId?: string; // Active org — the single blessed name. Matches the
// `organization_id` column + `current_user.organizationId` (RLS).
// The former `tenantId` alias was removed in v16.
// There is no `roles` here: `session.roles` was declared but
// never produced, and was retired in 17.0.0. Privilege
// is judged by the security service (permissions / positions /
// posture), never by a role-name string in a hook.
// No `roles` here (retired in 17.0.0) — privilege is judged
// by the security service, never by a role name in a hook.
accessToken?: string;
isSystem?: boolean; // Elevated system context (engine self-writes).
};
Expand Down
10 changes: 5 additions & 5 deletions skills/objectstack-data/rules/datasources.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ Full field reference: `node_modules/@objectstack/spec/src/data/datasource.zod.ts
| `external` | A mature external DB ObjectStack does **not** own; DDL forbidden; boot mismatch **fails**. |
| `validate-only` | Like `external`, but a mismatch **warns** instead of failing boot. |

`external` settings are required iff `schemaMode !== 'managed'` (and forbidden otherwise).
`external` settings are **required** when `schemaMode !== 'managed'`.

## Federated (external) objects

Expand All@@ -38,10 +38,10 @@ ObjectSchema.create({
### ✅ / ❌ Column mapping (ADR-0062 D7)

- ✅ Map remote columns with **`external.columnMap`** (`remoteColumn → localField`).
- ❌ **Never set `field.columnName` on an external object.** The driver's query
pipeline ignores it for federated objects, so it is a silent dual-source trap.
`os build` / `os validate` **rejects** it with a clear error. (`field.columnName`
on **managed** objects is unaffected.)
- ❌ **`field.columnName` does not exist — on ANY object.** It was removed in the
16.x line (the SQL driver hardcodes the physical column to the field key, so a
custom name was ignored), and authoring it is a parse error everywhere, not
only on a federated object.

## Auto-connect (no `onEnable`)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
18 changes: 8 additions & 10 deletions skills/objectstack-data/references/data-hooks.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -369,7 +369,7 @@ The sandbox is handed a **JSON snapshot** of these (built by
| `ctx.event` | string | e.g. `'afterUpdate'` — dispatch on it when one hook subscribes to several events. |
| `ctx.object` | string | The target object name. |
| `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. |
| `ctx.log` | `{ info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. |
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. |

Expand All@@ -395,6 +395,7 @@ org / user / transaction. Methods:
| `update(data, opts?)` | `api.write` | **`update({ id, ...fields })`** — put the id **inside** `data` |
| `upsert(data, opts?)` | `api.write` | `upsert({ … })` |
| `delete(opts)` | `api.write` | `delete({ where: { id } })` |
| `aggregate` · `updateMany` · `deleteMany` | read · write · write | also installed; same `where` shape |

**Query shape — the key is `where`.** It takes an object with `$`-operators, the
same DSL as the [objectstack-query](../../objectstack-query/SKILL.md) skill:
Expand DownExpand Up@@ -443,11 +444,11 @@ set of legal tokens (`HookBodyCapability`) is exactly five:

| Token | Unlocks |
|:--|:--|
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` |
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count` / `aggregate`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` / `updateMany` / `deleteMany` |
| `api.transaction` | `ctx.api.transaction(async () => { … })` — runs the callback's `ctx.api` ops in **one driver transaction** (commit on return, rollback on throw). Pair it with `api.write`. |
| `crypto.uuid` | `ctx.crypto.randomUUID()` |
| `log` | `ctx.log.info` / `warn` / `error(msg, data?)` |
| `log` | `ctx.log.debug` / `info` / `warn` / `error(msg, data?)` |

There is **no `http.fetch` capability** by design — outbound calls go through
Connector recipes so they stay auditable and replayable.
Expand DownExpand Up@@ -553,8 +554,7 @@ Register it like any hook — add it to `defineStack({ hooks: [fillPositionOnHir
capability it emits only when the runtime wired a logger into the hook context —
otherwise it is a **silent no-op**. Treat `ctx.log` as best-effort diagnostics,
not a reliable side-channel or proof a hook ran; to observe an effect, assert on
the data it writes. (And call `ctx.log.info(msg)` — `ctx.log` is an object, not
a function, so `ctx.log(msg)` is not callable.)
the data it writes.

---

Expand DownExpand Up@@ -589,10 +589,8 @@ interface HookContext {
organizationId?: string; // Active org — the single blessed name. Matches the
// `organization_id` column + `current_user.organizationId` (RLS).
// The former `tenantId` alias was removed in v16.
// There is no `roles` here: `session.roles` was declared but
// never produced, and was retired in 17.0.0. Privilege
// is judged by the security service (permissions / positions /
// posture), never by a role-name string in a hook.
// No `roles` here (retired in 17.0.0) — privilege is judged
// by the security service, never by a role name in a hook.
accessToken?: string;
isSystem?: boolean; // Elevated system context (engine self-writes).
};
Expand Down
10 changes: 5 additions & 5 deletions skills/objectstack-data/rules/datasources.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ Full field reference: `node_modules/@objectstack/spec/src/data/datasource.zod.ts
| `external` | A mature external DB ObjectStack does **not** own; DDL forbidden; boot mismatch **fails**. |
| `validate-only` | Like `external`, but a mismatch **warns** instead of failing boot. |

`external` settings are required iff `schemaMode !== 'managed'` (and forbidden otherwise).
`external` settings are **required** when `schemaMode !== 'managed'`.

## Federated (external) objects

Expand All@@ -38,10 +38,10 @@ ObjectSchema.create({
### ✅ / ❌ Column mapping (ADR-0062 D7)

- ✅ Map remote columns with **`external.columnMap`** (`remoteColumn → localField`).
- ❌ **Never set `field.columnName` on an external object.** The driver's query
pipeline ignores it for federated objects, so it is a silent dual-source trap.
`os build` / `os validate` **rejects** it with a clear error. (`field.columnName`
on **managed** objects is unaffected.)
- ❌ **`field.columnName` does not exist — on ANY object.** It was removed in the
16.x line (the SQL driver hardcodes the physical column to the field key, so a
custom name was ignored), and authoring it is a parse error everywhere, not
only on a federated object.

## Auto-connect (no `onEnable`)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
18 changes: 8 additions & 10 deletions skills/objectstack-data/references/data-hooks.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -369,7 +369,7 @@ The sandbox is handed a **JSON snapshot** of these (built by
| `ctx.event` | string | e.g. `'afterUpdate'` — dispatch on it when one hook subscribes to several events. |
| `ctx.object` | string | The target object name. |
| `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. |
| `ctx.log` | `{ info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. |
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. |

Expand All@@ -395,6 +395,7 @@ org / user / transaction. Methods:
| `update(data, opts?)` | `api.write` | **`update({ id, ...fields })`** — put the id **inside** `data` |
| `upsert(data, opts?)` | `api.write` | `upsert({ … })` |
| `delete(opts)` | `api.write` | `delete({ where: { id } })` |
| `aggregate` · `updateMany` · `deleteMany` | read · write · write | also installed; same `where` shape |

**Query shape — the key is `where`.** It takes an object with `$`-operators, the
same DSL as the [objectstack-query](../../objectstack-query/SKILL.md) skill:
Expand DownExpand Up@@ -443,11 +444,11 @@ set of legal tokens (`HookBodyCapability`) is exactly five:

| Token | Unlocks |
|:--|:--|
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` |
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count` / `aggregate`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` / `updateMany` / `deleteMany` |
| `api.transaction` | `ctx.api.transaction(async () => { … })` — runs the callback's `ctx.api` ops in **one driver transaction** (commit on return, rollback on throw). Pair it with `api.write`. |
| `crypto.uuid` | `ctx.crypto.randomUUID()` |
| `log` | `ctx.log.info` / `warn` / `error(msg, data?)` |
| `log` | `ctx.log.debug` / `info` / `warn` / `error(msg, data?)` |

There is **no `http.fetch` capability** by design — outbound calls go through
Connector recipes so they stay auditable and replayable.
Expand DownExpand Up@@ -553,8 +554,7 @@ Register it like any hook — add it to `defineStack({ hooks: [fillPositionOnHir
capability it emits only when the runtime wired a logger into the hook context —
otherwise it is a **silent no-op**. Treat `ctx.log` as best-effort diagnostics,
not a reliable side-channel or proof a hook ran; to observe an effect, assert on
the data it writes. (And call `ctx.log.info(msg)` — `ctx.log` is an object, not
a function, so `ctx.log(msg)` is not callable.)
the data it writes.

---

Expand DownExpand Up@@ -589,10 +589,8 @@ interface HookContext {
organizationId?: string; // Active org — the single blessed name. Matches the
// `organization_id` column + `current_user.organizationId` (RLS).
// The former `tenantId` alias was removed in v16.
// There is no `roles` here: `session.roles` was declared but
// never produced, and was retired in 17.0.0. Privilege
// is judged by the security service (permissions / positions /
// posture), never by a role-name string in a hook.
// No `roles` here (retired in 17.0.0) — privilege is judged
// by the security service, never by a role name in a hook.
accessToken?: string;
isSystem?: boolean; // Elevated system context (engine self-writes).
};
Expand Down
10 changes: 5 additions & 5 deletions skills/objectstack-data/rules/datasources.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ Full field reference: `node_modules/@objectstack/spec/src/data/datasource.zod.ts
| `external` | A mature external DB ObjectStack does **not** own; DDL forbidden; boot mismatch **fails**. |
| `validate-only` | Like `external`, but a mismatch **warns** instead of failing boot. |

`external` settings are required iff `schemaMode !== 'managed'` (and forbidden otherwise).
`external` settings are **required** when `schemaMode !== 'managed'`.

## Federated (external) objects

Expand All@@ -38,10 +38,10 @@ ObjectSchema.create({
### ✅ / ❌ Column mapping (ADR-0062 D7)

- ✅ Map remote columns with **`external.columnMap`** (`remoteColumn → localField`).
- ❌ **Never set `field.columnName` on an external object.** The driver's query
pipeline ignores it for federated objects, so it is a silent dual-source trap.
`os build` / `os validate` **rejects** it with a clear error. (`field.columnName`
on **managed** objects is unaffected.)
- ❌ **`field.columnName` does not exist — on ANY object.** It was removed in the
16.x line (the SQL driver hardcodes the physical column to the field key, so a
custom name was ignored), and authoring it is a parse error everywhere, not
only on a federated object.

## Auto-connect (no `onEnable`)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
18 changes: 8 additions & 10 deletions skills/objectstack-data/references/data-hooks.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -369,7 +369,7 @@ The sandbox is handed a **JSON snapshot** of these (built by
| `ctx.event` | string | e.g. `'afterUpdate'` — dispatch on it when one hook subscribes to several events. |
| `ctx.object` | string | The target object name. |
| `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. |
| `ctx.log` | `{ info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. |
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. |

Expand All@@ -395,6 +395,7 @@ org / user / transaction. Methods:
| `update(data, opts?)` | `api.write` | **`update({ id, ...fields })`** — put the id **inside** `data` |
| `upsert(data, opts?)` | `api.write` | `upsert({ … })` |
| `delete(opts)` | `api.write` | `delete({ where: { id } })` |
| `aggregate` · `updateMany` · `deleteMany` | read · write · write | also installed; same `where` shape |

**Query shape — the key is `where`.** It takes an object with `$`-operators, the
same DSL as the [objectstack-query](../../objectstack-query/SKILL.md) skill:
Expand DownExpand Up@@ -443,11 +444,11 @@ set of legal tokens (`HookBodyCapability`) is exactly five:

| Token | Unlocks |
|:--|:--|
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` |
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count` / `aggregate`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` / `updateMany` / `deleteMany` |
| `api.transaction` | `ctx.api.transaction(async () => { … })` — runs the callback's `ctx.api` ops in **one driver transaction** (commit on return, rollback on throw). Pair it with `api.write`. |
| `crypto.uuid` | `ctx.crypto.randomUUID()` |
| `log` | `ctx.log.info` / `warn` / `error(msg, data?)` |
| `log` | `ctx.log.debug` / `info` / `warn` / `error(msg, data?)` |

There is **no `http.fetch` capability** by design — outbound calls go through
Connector recipes so they stay auditable and replayable.
Expand DownExpand Up@@ -553,8 +554,7 @@ Register it like any hook — add it to `defineStack({ hooks: [fillPositionOnHir
capability it emits only when the runtime wired a logger into the hook context —
otherwise it is a **silent no-op**. Treat `ctx.log` as best-effort diagnostics,
not a reliable side-channel or proof a hook ran; to observe an effect, assert on
the data it writes. (And call `ctx.log.info(msg)` — `ctx.log` is an object, not
a function, so `ctx.log(msg)` is not callable.)
the data it writes.

---

Expand DownExpand Up@@ -589,10 +589,8 @@ interface HookContext {
organizationId?: string; // Active org — the single blessed name. Matches the
// `organization_id` column + `current_user.organizationId` (RLS).
// The former `tenantId` alias was removed in v16.
// There is no `roles` here: `session.roles` was declared but
// never produced, and was retired in 17.0.0. Privilege
// is judged by the security service (permissions / positions /
// posture), never by a role-name string in a hook.
// No `roles` here (retired in 17.0.0) — privilege is judged
// by the security service, never by a role name in a hook.
accessToken?: string;
isSystem?: boolean; // Elevated system context (engine self-writes).
};
Expand Down
10 changes: 5 additions & 5 deletions skills/objectstack-data/rules/datasources.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ Full field reference: `node_modules/@objectstack/spec/src/data/datasource.zod.ts
| `external` | A mature external DB ObjectStack does **not** own; DDL forbidden; boot mismatch **fails**. |
| `validate-only` | Like `external`, but a mismatch **warns** instead of failing boot. |

`external` settings are required iff `schemaMode !== 'managed'` (and forbidden otherwise).
`external` settings are **required** when `schemaMode !== 'managed'`.

## Federated (external) objects

Expand All@@ -38,10 +38,10 @@ ObjectSchema.create({
### ✅ / ❌ Column mapping (ADR-0062 D7)

- ✅ Map remote columns with **`external.columnMap`** (`remoteColumn → localField`).
- ❌ **Never set `field.columnName` on an external object.** The driver's query
pipeline ignores it for federated objects, so it is a silent dual-source trap.
`os build` / `os validate` **rejects** it with a clear error. (`field.columnName`
on **managed** objects is unaffected.)
- ❌ **`field.columnName` does not exist — on ANY object.** It was removed in the
16.x line (the SQL driver hardcodes the physical column to the field key, so a
custom name was ignored), and authoring it is a parse error everywhere, not
only on a federated object.

## Auto-connect (no `onEnable`)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
18 changes: 8 additions & 10 deletions skills/objectstack-data/references/data-hooks.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -369,7 +369,7 @@ The sandbox is handed a **JSON snapshot** of these (built by
| `ctx.event` | string | e.g. `'afterUpdate'` — dispatch on it when one hook subscribes to several events. |
| `ctx.object` | string | The target object name. |
| `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. |
| `ctx.log` | `{ info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
| `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. |
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. |

Expand All@@ -395,6 +395,7 @@ org / user / transaction. Methods:
| `update(data, opts?)` | `api.write` | **`update({ id, ...fields })`** — put the id **inside** `data` |
| `upsert(data, opts?)` | `api.write` | `upsert({ … })` |
| `delete(opts)` | `api.write` | `delete({ where: { id } })` |
| `aggregate` · `updateMany` · `deleteMany` | read · write · write | also installed; same `where` shape |

**Query shape — the key is `where`.** It takes an object with `$`-operators, the
same DSL as the [objectstack-query](../../objectstack-query/SKILL.md) skill:
Expand DownExpand Up@@ -443,11 +444,11 @@ set of legal tokens (`HookBodyCapability`) is exactly five:

| Token | Unlocks |
|:--|:--|
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` |
| `api.read` | `ctx.api.object(n).find` / `findOne` / `count` / `aggregate`; also `ctx.title('<lookup field>')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. |
| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` / `updateMany` / `deleteMany` |
| `api.transaction` | `ctx.api.transaction(async () => { … })` — runs the callback's `ctx.api` ops in **one driver transaction** (commit on return, rollback on throw). Pair it with `api.write`. |
| `crypto.uuid` | `ctx.crypto.randomUUID()` |
| `log` | `ctx.log.info` / `warn` / `error(msg, data?)` |
| `log` | `ctx.log.debug` / `info` / `warn` / `error(msg, data?)` |

There is **no `http.fetch` capability** by design — outbound calls go through
Connector recipes so they stay auditable and replayable.
Expand DownExpand Up@@ -553,8 +554,7 @@ Register it like any hook — add it to `defineStack({ hooks: [fillPositionOnHir
capability it emits only when the runtime wired a logger into the hook context —
otherwise it is a **silent no-op**. Treat `ctx.log` as best-effort diagnostics,
not a reliable side-channel or proof a hook ran; to observe an effect, assert on
the data it writes. (And call `ctx.log.info(msg)` — `ctx.log` is an object, not
a function, so `ctx.log(msg)` is not callable.)
the data it writes.

---

Expand DownExpand Up@@ -589,10 +589,8 @@ interface HookContext {
organizationId?: string; // Active org — the single blessed name. Matches the
// `organization_id` column + `current_user.organizationId` (RLS).
// The former `tenantId` alias was removed in v16.
// There is no `roles` here: `session.roles` was declared but
// never produced, and was retired in 17.0.0. Privilege
// is judged by the security service (permissions / positions /
// posture), never by a role-name string in a hook.
// No `roles` here (retired in 17.0.0) — privilege is judged
// by the security service, never by a role name in a hook.
accessToken?: string;
isSystem?: boolean; // Elevated system context (engine self-writes).
};
Expand Down
10 changes: 5 additions & 5 deletions skills/objectstack-data/rules/datasources.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ Full field reference: `node_modules/@objectstack/spec/src/data/datasource.zod.ts
| `external` | A mature external DB ObjectStack does **not** own; DDL forbidden; boot mismatch **fails**. |
| `validate-only` | Like `external`, but a mismatch **warns** instead of failing boot. |

`external` settings are required iff `schemaMode !== 'managed'` (and forbidden otherwise).
`external` settings are **required** when `schemaMode !== 'managed'`.

## Federated (external) objects

Expand All@@ -38,10 +38,10 @@ ObjectSchema.create({
### ✅ / ❌ Column mapping (ADR-0062 D7)

- ✅ Map remote columns with **`external.columnMap`** (`remoteColumn → localField`).
- ❌ **Never set `field.columnName` on an external object.** The driver's query
pipeline ignores it for federated objects, so it is a silent dual-source trap.
`os build` / `os validate` **rejects** it with a clear error. (`field.columnName`
on **managed** objects is unaffected.)
- ❌ **`field.columnName` does not exist — on ANY object.** It was removed in the
16.x line (the SQL driver hardcodes the physical column to the field key, so a
custom name was ignored), and authoring it is a parse error everywhere, not
only on a federated object.

## Auto-connect (no `onEnable`)

Expand Down
Loading