makersclaw 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -8
- package/package.json +1 -1
- package/r/annotate.json +4 -4
- package/r/approval-request.json +3 -3
- package/r/audio-player.json +3 -3
- package/r/auth-form.json +2 -2
- package/r/board.json +4 -4
- package/r/calendar-view.json +4 -4
- package/r/cart.json +3 -3
- package/r/chart.json +3 -3
- package/r/chat.json +3 -3
- package/r/code-editor.json +4 -4
- package/r/comment-box.json +4 -4
- package/r/contact-section.json +3 -3
- package/r/core.json +3 -3
- package/r/csv-import.json +4 -4
- package/r/data-table.json +3 -3
- package/r/diagram.json +3 -3
- package/r/doc-editor.json +4 -4
- package/r/doc-page.json +4 -4
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +3 -3
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +3 -3
- package/r/gantt.json +4 -4
- package/r/job-panel.json +3 -3
- package/r/json-editor.json +4 -4
- package/r/map.json +5 -5
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +6 -6
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +6 -6
- package/r/product-detail.json +3 -3
- package/r/product-grid.json +4 -4
- package/r/record-form.json +3 -3
- package/r/record-notes.json +5 -5
- package/r/registry.json +121 -121
- package/r/search-dialog.json +3 -3
- package/r/settings-list.json +3 -3
- package/r/setup-checklist.json +3 -3
- package/r/share-dialog.json +3 -3
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +5 -5
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +4 -4
- package/r/task-list.json +4 -4
- package/r/task-model.json +3 -3
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +3 -3
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +14 -5
- package/registry/blocks/approval-request/README.md +15 -8
- package/registry/blocks/audio-player/README.md +17 -10
- package/registry/blocks/board/README.md +13 -9
- package/registry/blocks/board/board.jsx +1 -1
- package/registry/blocks/calendar-view/README.md +28 -4
- package/registry/blocks/cart/README.md +14 -10
- package/registry/blocks/chart/README.md +14 -7
- package/registry/blocks/chat/README.md +43 -13
- package/registry/blocks/code-editor/README.md +4 -0
- package/registry/blocks/code-editor/code-editor.jsx +1 -1
- package/registry/blocks/comment-box/README.md +20 -13
- package/registry/blocks/contact-section/README.md +7 -6
- package/registry/blocks/csv-import/README.md +34 -12
- package/registry/blocks/data-table/README.md +41 -8
- package/registry/blocks/diagram/diagram.jsx +1 -1
- package/registry/blocks/doc-editor/README.md +18 -6
- package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
- package/registry/blocks/doc-page/README.md +16 -6
- package/registry/blocks/file-upload/README.md +7 -1
- package/registry/blocks/flow-canvas/README.md +16 -5
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +4 -2
- package/registry/blocks/gantt/README.md +18 -12
- package/registry/blocks/job-panel/README.md +1 -1
- package/registry/blocks/json-editor/README.md +16 -4
- package/registry/blocks/map/README.md +19 -3
- package/registry/blocks/map/map-view.jsx +1 -1
- package/registry/blocks/map/maplibre.js +1 -1
- package/registry/blocks/mind-map/README.md +16 -5
- package/registry/blocks/org-chart/README.md +10 -7
- package/registry/blocks/page-builder/README.md +13 -6
- package/registry/blocks/page-builder/page-builder.jsx +1 -1
- package/registry/blocks/page-builder/page-view.jsx +1 -1
- package/registry/blocks/page-builder/page.js +1 -1
- package/registry/blocks/pdf-viewer/README.md +22 -20
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
- package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
- package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
- package/registry/blocks/product-detail/README.md +13 -7
- package/registry/blocks/product-detail/demo.jsx +1 -1
- package/registry/blocks/product-grid/README.md +12 -10
- package/registry/blocks/record-form/README.md +26 -9
- package/registry/blocks/record-notes/README.md +42 -30
- package/registry/blocks/search-dialog/README.md +38 -7
- package/registry/blocks/settings-list/README.md +29 -17
- package/registry/blocks/setup-checklist/README.md +12 -14
- package/registry/blocks/share-dialog/README.md +1 -1
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +13 -4
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
- package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
- package/registry/blocks/spreadsheet/README.md +17 -8
- package/registry/blocks/stat-cards/README.md +17 -8
- package/registry/blocks/task-list/README.md +14 -11
- package/registry/blocks/task-model/README.md +3 -3
- package/registry/blocks/transaction-list/README.md +15 -7
- package/registry/blocks/whiteboard/README.md +19 -13
- package/src/cli.mjs +31 -4
- package/src/install.mjs +263 -21
- package/src/registry.mjs +78 -1
- package/templates/next/browser/app/auto-refresh.jsx +67 -0
- package/templates/next/browser/app/layout.jsx +94 -0
- package/templates/next/browser/app/page.jsx +60 -0
- package/templates/next/browser/app/s/[key]/page.jsx +95 -0
- package/templates/next/browser/lib/actions.js +16 -0
- package/templates/next/browser/lib/data.js +43 -0
- package/templates/next/files/README.md +810 -79
- package/templates/next/files/app/frame.js +128 -4
- package/templates/next/files/check/run.mjs +1438 -41
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/notice.jsx +1 -1
- package/templates/next/files/components/ui/separator.jsx +1 -1
- package/templates/next/files/lib/actions.js +170 -51
- package/templates/next/files/lib/brain-server.js +96 -0
- package/templates/next/files/lib/brain.js +413 -0
- package/templates/next/files/lib/claw.js +11 -0
- package/templates/next/files/lib/data.js +119 -29
- package/templates/next/files/lib/db.js +97 -17
- package/templates/next/files/lib/door.js +680 -0
- package/templates/next/files/next.config.mjs +36 -2
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/template.json +9 -3
- package/templates/next/files/schema.sql +0 -22
|
@@ -17,7 +17,7 @@ import { Icon } from "./icon";
|
|
|
17
17
|
// meta a count, a date, a Badge — muted, on the right
|
|
18
18
|
// actions buttons on the far right (IconButton, Menu); they sit above
|
|
19
19
|
// the row's target, so they click on their own
|
|
20
|
-
// href a next/link row · `onClick` a button row · neither:
|
|
20
|
+
// href a next/link row · `onClick` a button row · neither: a plain row
|
|
21
21
|
// size default (56px) · sm (44px, for dense lists and rails) ·
|
|
22
22
|
// lg (64px, roomier: a pipeline's runs, a list read at leisure)
|
|
23
23
|
// variant plain (no fill) · outline (a bordered card) · muted (a filled panel)
|
|
@@ -58,7 +58,7 @@ const STACKED = {
|
|
|
58
58
|
|
|
59
59
|
const TARGET = "outline-none after:absolute after:inset-0";
|
|
60
60
|
|
|
61
|
-
// The row filled while its target (or a
|
|
61
|
+
// The row filled while its target (or a plain row itself) is current.
|
|
62
62
|
const CURRENT =
|
|
63
63
|
"has-[[data-item-target][aria-current]:not([aria-current=false])]:bg-accent " +
|
|
64
64
|
"[&[aria-current]:not([aria-current=false])]:bg-accent";
|
|
@@ -2,7 +2,7 @@ import { cx } from "./cx";
|
|
|
2
2
|
import { IconButton } from "./button";
|
|
3
3
|
import { Icon } from "./icon";
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Plain parts: a card, a notice, a badge, a kicker, the empty state.
|
|
6
6
|
// Server-safe (no hooks), so a server surface can use them directly.
|
|
7
7
|
|
|
8
8
|
const TONES = {
|
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
import { headers } from "next/headers";
|
|
4
4
|
import { revalidatePath } from "next/cache";
|
|
5
|
-
import { ident, logActivity, query } from "./db";
|
|
5
|
+
import { ident, logActivity, query, requestMember, transaction } from "./db";
|
|
6
6
|
|
|
7
7
|
// Every write the app makes. Callable from server and client components
|
|
8
8
|
// alike (they are server actions). Each one goes through lib/db.js,
|
|
9
9
|
// records an activity row, and re-renders the app. Table and column names
|
|
10
10
|
// are quoted through ident(), which refuses anything that is not a plain
|
|
11
|
-
// identifier. Returns { ok } or { ok: false, error }; never throws.
|
|
11
|
+
// identifier. Returns { ok } or { ok: false, error, code }; never throws.
|
|
12
|
+
// `code` is the one the platform's data door answers a browser app with
|
|
13
|
+
// (lib/door.js), so a surface branches on it the same way in both modes.
|
|
14
|
+
//
|
|
15
|
+
// Each write runs in its own transaction that first tells the database
|
|
16
|
+
// who is asking (transaction() in lib/db.js: claw.member_id and
|
|
17
|
+
// claw.member_email, local to it), so a trigger or an action function
|
|
18
|
+
// reads the member here as it does in a browser app, behind the platform's
|
|
19
|
+
// door. The activity line is written after the COMMIT, never inside it:
|
|
20
|
+
// a line that fails must not undo the write it describes.
|
|
12
21
|
//
|
|
13
22
|
// `surface` is optional: proxy.js stamps the current surface key on
|
|
14
23
|
// the request, so a surface rarely needs to pass it.
|
|
@@ -20,13 +29,12 @@ import { ident, logActivity, query } from "./db";
|
|
|
20
29
|
// words. Pass a detail when the surface can say what changed in plain
|
|
21
30
|
// words; leave it out and the tab shows the action word alone.
|
|
22
31
|
|
|
32
|
+
/** The line's `by`, as the door writes it: the member's email, else
|
|
33
|
+
* their id, else unknown (a local run). */
|
|
23
34
|
async function actor() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} catch {
|
|
28
|
-
return "user:unknown";
|
|
29
|
-
}
|
|
35
|
+
const { id, email } = await requestMember();
|
|
36
|
+
if (email) return `user:${email}`;
|
|
37
|
+
return id ? `user:${id}` : "user:unknown";
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
async function surfaceOf(explicit) {
|
|
@@ -97,36 +105,88 @@ async function setClause(source, cols) {
|
|
|
97
105
|
return cols.map((c, i) => `${ident(c)} = $${i + 2}`).join(", ") + (await stampFor(source, cols));
|
|
98
106
|
}
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
// Postgres's raise_exception: what a plpgsql `raise exception` says.
|
|
109
|
+
const REFUSED = "P0001";
|
|
110
|
+
|
|
111
|
+
// The data door's codes for Postgres's errors (claw's STATIC-SPEC §1,
|
|
112
|
+
// "Errors reach the Builder"). Up to 0.6.x a failure here had no code, so
|
|
113
|
+
// a surface that branched on the door's `code` in a browser app fell
|
|
114
|
+
// through to its generic path after `makersclaw mode server`.
|
|
115
|
+
const CODES = new Map([
|
|
116
|
+
[REFUSED, "action_refused"],
|
|
117
|
+
["23505", "conflict"], // unique
|
|
118
|
+
["23503", "conflict"], // foreign key
|
|
119
|
+
["23502", "bad_request"], // not null
|
|
120
|
+
["22P02", "bad_request"], // not a value of the column's type
|
|
121
|
+
["22007", "bad_request"], // not a date or time
|
|
122
|
+
["22008", "bad_request"], // a date or time out of range
|
|
123
|
+
["57014", "too_slow"],
|
|
124
|
+
["40001", "try_again"], // serialization
|
|
125
|
+
["40P01", "try_again"], // deadlock
|
|
126
|
+
["55P03", "try_again"], // lock not available
|
|
127
|
+
]);
|
|
128
|
+
const SQLSTATE = /^[0-9A-Z]{5}$/;
|
|
129
|
+
|
|
130
|
+
/** A failure's code, as the door gives it. `missing` names the one error
|
|
131
|
+
* that means the statement's own table or function is not there: 42P01
|
|
132
|
+
* no_such_table for a table's write, 42883 no_such_action for an
|
|
133
|
+
* action. Postgres raises it with no `where` (the context an error from
|
|
134
|
+
* inside a function or a trigger carries); from inside, it is the
|
|
135
|
+
* code's fault, bad_request, as the rest of class 42. ident()'s refusal
|
|
136
|
+
* is bad_request; a connection that failed is failed. */
|
|
137
|
+
function codeOf(error, missing) {
|
|
138
|
+
const state = typeof error?.code === "string" && SQLSTATE.test(error.code) ? error.code : "";
|
|
139
|
+
if (!state) return /^invalid identifier: /.test(error?.message ?? "") ? "bad_request" : "failed";
|
|
140
|
+
if (missing?.[state] && !error.where) return missing[state];
|
|
141
|
+
if (CODES.has(state)) return CODES.get(state);
|
|
142
|
+
return state.startsWith("42") ? "bad_request" : "failed";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function fail(error, missing) {
|
|
101
146
|
const message = error?.message ?? String(error);
|
|
102
|
-
|
|
103
|
-
|
|
147
|
+
const code = codeOf(error, missing);
|
|
148
|
+
// A trigger's own raise exception is a deliberate no, as an action's is
|
|
149
|
+
// (runAction): a warning, or the log tail sends the platform agent after it.
|
|
150
|
+
if (code === "action_refused") console.warn(`[actions] refused: ${message}`);
|
|
151
|
+
else console.error(`[actions] write failed: ${message}`);
|
|
152
|
+
return { ok: false, error: message, code };
|
|
104
153
|
}
|
|
105
154
|
|
|
155
|
+
/** A write refused here, with the door's code for it. */
|
|
156
|
+
const refused = (error, code) => ({ ok: false, error, code });
|
|
157
|
+
|
|
158
|
+
/** What the door reads a table's write failing with, when the table is not there. */
|
|
159
|
+
const NO_TABLE = { "42P01": "no_such_table" };
|
|
160
|
+
|
|
106
161
|
/** Change columns of one row. `patch` is { column: value }; updated_at
|
|
107
|
-
* becomes now() when the table has one (above).
|
|
108
|
-
|
|
162
|
+
* becomes now() when the table has one (above). `log: false` writes no
|
|
163
|
+
* activity line: for a save that is typing, not a decision (an autosave
|
|
164
|
+
* every few keystrokes would flood the Activity tab). */
|
|
165
|
+
export async function updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data, log }) {
|
|
109
166
|
try {
|
|
167
|
+
const table = ident(source);
|
|
168
|
+
const key = ident(idColumn);
|
|
110
169
|
const cols = Object.keys(patch ?? {}).filter((c) => patch[c] !== undefined);
|
|
111
|
-
if (!cols.length) return
|
|
170
|
+
if (!cols.length) return refused("nothing to change", "bad_request");
|
|
112
171
|
const sets = await setClause(source, cols);
|
|
113
|
-
const { rowCount } = await
|
|
114
|
-
`UPDATE ${
|
|
115
|
-
[id, ...cols.map((c) => patch[c])],
|
|
172
|
+
const { rowCount } = await transaction((q) =>
|
|
173
|
+
q(`UPDATE ${table} SET ${sets} WHERE ${key} = $1`, [id, ...cols.map((c) => patch[c])]),
|
|
116
174
|
);
|
|
117
|
-
if (!rowCount) return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
175
|
+
if (!rowCount) return refused("row not found", "not_found");
|
|
176
|
+
if (log !== false) {
|
|
177
|
+
await logActivity({
|
|
178
|
+
action,
|
|
179
|
+
surface: await surfaceOf(surface),
|
|
180
|
+
row_ref: id,
|
|
181
|
+
detail: detail ?? `${cols.join(", ")} changed`,
|
|
182
|
+
data: data ?? patch,
|
|
183
|
+
by: await actor(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
126
186
|
done();
|
|
127
187
|
return { ok: true };
|
|
128
188
|
} catch (error) {
|
|
129
|
-
return fail(error);
|
|
189
|
+
return fail(error, NO_TABLE);
|
|
130
190
|
}
|
|
131
191
|
}
|
|
132
192
|
|
|
@@ -148,12 +208,10 @@ export async function moveRow({ source, idColumn = "id", id, stateColumn, to, su
|
|
|
148
208
|
export async function insertRow({ source, idColumn = "id", values, surface, action = "added", detail }) {
|
|
149
209
|
try {
|
|
150
210
|
const cols = Object.keys(values ?? {}).filter((c) => values[c] !== undefined);
|
|
151
|
-
if (!cols.length) return
|
|
211
|
+
if (!cols.length) return refused("nothing to add", "bad_request");
|
|
152
212
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
153
|
-
const {
|
|
154
|
-
|
|
155
|
-
cols.map((c) => values[c]),
|
|
156
|
-
);
|
|
213
|
+
const text = `INSERT INTO ${ident(source)} (${cols.map(ident).join(", ")}) VALUES (${placeholders}) RETURNING ${ident(idColumn)}`;
|
|
214
|
+
const { rows } = await transaction((q) => q(text, cols.map((c) => values[c])));
|
|
157
215
|
const id = rows[0]?.[idColumn];
|
|
158
216
|
await logActivity({
|
|
159
217
|
action,
|
|
@@ -166,14 +224,15 @@ export async function insertRow({ source, idColumn = "id", values, surface, acti
|
|
|
166
224
|
done();
|
|
167
225
|
return { ok: true, id };
|
|
168
226
|
} catch (error) {
|
|
169
|
-
return fail(error);
|
|
227
|
+
return fail(error, NO_TABLE);
|
|
170
228
|
}
|
|
171
229
|
}
|
|
172
230
|
|
|
173
231
|
export async function deleteRow({ source, idColumn = "id", id, surface, detail }) {
|
|
174
232
|
try {
|
|
175
|
-
const
|
|
176
|
-
|
|
233
|
+
const text = `DELETE FROM ${ident(source)} WHERE ${ident(idColumn)} = $1`;
|
|
234
|
+
const { rowCount } = await transaction((q) => q(text, [id]));
|
|
235
|
+
if (!rowCount) return refused("row not found", "not_found");
|
|
177
236
|
await logActivity({
|
|
178
237
|
action: "removed",
|
|
179
238
|
surface: await surfaceOf(surface),
|
|
@@ -184,7 +243,7 @@ export async function deleteRow({ source, idColumn = "id", id, surface, detail }
|
|
|
184
243
|
done();
|
|
185
244
|
return { ok: true };
|
|
186
245
|
} catch (error) {
|
|
187
|
-
return fail(error);
|
|
246
|
+
return fail(error, NO_TABLE);
|
|
188
247
|
}
|
|
189
248
|
}
|
|
190
249
|
|
|
@@ -195,7 +254,8 @@ export async function deleteRow({ source, idColumn = "id", id, surface, detail }
|
|
|
195
254
|
* has a plain `key text` column and Postgres rejects ON CONFLICT without a
|
|
196
255
|
* unique index on it. JSON mode sets the key inside the jsonb `column` of
|
|
197
256
|
* the row `id` in `source`. An update stamps updated_at as updateRow's
|
|
198
|
-
* does; the insert leaves it to the column's default.
|
|
257
|
+
* does; the insert leaves it to the column's default. The UPDATE and the
|
|
258
|
+
* INSERT run in one transaction (up to 0.6.x they were two).
|
|
199
259
|
*/
|
|
200
260
|
export async function saveSetting({
|
|
201
261
|
mode = "table",
|
|
@@ -210,24 +270,21 @@ export async function saveSetting({
|
|
|
210
270
|
surface,
|
|
211
271
|
}) {
|
|
212
272
|
try {
|
|
213
|
-
if (!key) return
|
|
273
|
+
if (!key) return refused("no key", "bad_request");
|
|
214
274
|
if (mode === "json") {
|
|
275
|
+
const col = ident(column);
|
|
215
276
|
const stamp = await stampFor(source, [column]);
|
|
216
|
-
const {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
);
|
|
220
|
-
if (!rowCount) return { ok: false, error: "row not found" };
|
|
277
|
+
const text = `UPDATE ${ident(source)} SET ${col} = jsonb_set(COALESCE(${col}, '{}'::jsonb), $2::text[], $3::jsonb, true)${stamp} WHERE ${ident(idColumn)} = $1`;
|
|
278
|
+
const { rowCount } = await transaction((q) => q(text, [id, [key], JSON.stringify(value ?? null)]));
|
|
279
|
+
if (!rowCount) return refused("row not found", "not_found");
|
|
221
280
|
} else {
|
|
222
281
|
const stored = typeof value === "object" && value !== null ? JSON.stringify(value) : value;
|
|
282
|
+
const [table, keyCol, valueCol] = [ident(source), ident(keyColumn), ident(valueColumn)];
|
|
223
283
|
const stamp = await stampFor(source, [valueColumn]);
|
|
224
|
-
|
|
225
|
-
`UPDATE ${
|
|
226
|
-
[key, stored]
|
|
227
|
-
);
|
|
228
|
-
if (!rowCount) {
|
|
229
|
-
await query(`INSERT INTO ${ident(source)} (${ident(keyColumn)}, ${ident(valueColumn)}) VALUES ($1, $2)`, [key, stored]);
|
|
230
|
-
}
|
|
284
|
+
await transaction(async (q) => {
|
|
285
|
+
const { rowCount } = await q(`UPDATE ${table} SET ${valueCol} = $2${stamp} WHERE ${keyCol} = $1`, [key, stored]);
|
|
286
|
+
if (!rowCount) await q(`INSERT INTO ${table} (${keyCol}, ${valueCol}) VALUES ($1, $2)`, [key, stored]);
|
|
287
|
+
});
|
|
231
288
|
}
|
|
232
289
|
await logActivity({
|
|
233
290
|
action: "edited",
|
|
@@ -240,6 +297,68 @@ export async function saveSetting({
|
|
|
240
297
|
done();
|
|
241
298
|
return { ok: true };
|
|
242
299
|
} catch (error) {
|
|
243
|
-
return fail(error);
|
|
300
|
+
return fail(error, NO_TABLE);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// An action's name as the door takes it: one bare, lower-case name. The
|
|
305
|
+
// door calls only a function of the app's own schema, so a name with a
|
|
306
|
+
// schema (`app_x.fn`) or capitals (`CloseCycle`), which ident() quotes
|
|
307
|
+
// and Postgres would find, worked here and is no_such_action there.
|
|
308
|
+
// Refused here too, so a surface that works in one mode works in the
|
|
309
|
+
// other.
|
|
310
|
+
const ACTION_NAME = /^[a-z_][a-z0-9_]{0,62}$/;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Run an action: a function in the app's own schema, `(args jsonb)
|
|
314
|
+
* returns jsonb`, for any write of more than one row, or one that decides
|
|
315
|
+
* something. The screen calls it here; the app's agent runs the same
|
|
316
|
+
* function with `sql` (`select close_cycle($1::jsonb)`), so the two never
|
|
317
|
+
* drift. It runs inside transaction(), after the member is set, so it
|
|
318
|
+
* reads who is asking from `current_setting('claw.member_email', true)`.
|
|
319
|
+
* It says no with `raise exception '<sentence>'`: the sentence comes back
|
|
320
|
+
* as `error`, code action_refused, and the log says it was refused, not
|
|
321
|
+
* that the app failed. A name that is not one bare lower-case name, or no
|
|
322
|
+
* function of it that takes jsonb, is no_such_action, as at the door.
|
|
323
|
+
*
|
|
324
|
+
* The activity line is this call's: `action` defaults to the function's
|
|
325
|
+
* name, `row_ref` is `rowRef` or the result's `id`. The function never
|
|
326
|
+
* writes claw_app.activity itself. `log: false` writes no line (an
|
|
327
|
+
* autosave). `args` defaults to {}.
|
|
328
|
+
* → { ok: true, result } | { ok: false, error, code }
|
|
329
|
+
*/
|
|
330
|
+
export async function runAction({ name, args, surface, action, detail, rowRef, log } = {}) {
|
|
331
|
+
if (typeof name !== "string" || !ACTION_NAME.test(name)) {
|
|
332
|
+
const error = `no action ${JSON.stringify(name ?? null)}: an action is named in lower case, a-z, 0-9 and _, with no schema (close_cycle)`;
|
|
333
|
+
console.error(`[actions] ${error}`);
|
|
334
|
+
return refused(error, "no_such_action");
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const text = `SELECT ${ident(name)}($1::jsonb) AS result`;
|
|
338
|
+
const json = JSON.stringify(args === undefined ? {} : args);
|
|
339
|
+
const { rows } = await transaction((q) => q(text, [json]));
|
|
340
|
+
const result = rows[0]?.result ?? null;
|
|
341
|
+
if (log !== false) {
|
|
342
|
+
const returned = result && typeof result === "object" && !Array.isArray(result) ? result.id : undefined;
|
|
343
|
+
await logActivity({
|
|
344
|
+
action: action || name,
|
|
345
|
+
surface: await surfaceOf(surface),
|
|
346
|
+
row_ref: rowRef ?? returned ?? null,
|
|
347
|
+
detail: detail ?? null,
|
|
348
|
+
by: await actor(),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
done();
|
|
352
|
+
return { ok: true, result };
|
|
353
|
+
} catch (error) {
|
|
354
|
+
// P0001 is the function's own raise exception: a deliberate no ("that
|
|
355
|
+
// cycle is already closed"), which the member acts on. Logged as an
|
|
356
|
+
// error, the log tail would send the platform agent chasing it as a
|
|
357
|
+
// bug, which the door avoids by not recording it either.
|
|
358
|
+
if (error?.code === REFUSED) {
|
|
359
|
+
console.warn(`[actions] ${name} refused: ${error.message}`);
|
|
360
|
+
return refused(error.message, "action_refused");
|
|
361
|
+
}
|
|
362
|
+
return fail(error, { "42883": "no_such_action" });
|
|
244
363
|
}
|
|
245
364
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { headers } from "next/headers";
|
|
2
|
+
import { clawAnswer } from "./claw";
|
|
3
|
+
import { brainRead } from "./brain";
|
|
4
|
+
|
|
5
|
+
// The company brain's reads, from a SERVER component, in a server app.
|
|
6
|
+
// The same nine reads as lib/brain.js, with the same arguments and
|
|
7
|
+
// results (claw docs/brain/CONTRACTS.md §10); no writes. A write from the
|
|
8
|
+
// server would be app code acting, never the member's press, so writes go
|
|
9
|
+
// from the browser through lib/brain.js even here.
|
|
10
|
+
//
|
|
11
|
+
// The browser's door grant is its cookie, which never reaches this
|
|
12
|
+
// server. Here the grant is the bearer the product proxy stamps on each
|
|
13
|
+
// member's request as `x-claw-door` (5 minutes, bound to this app and
|
|
14
|
+
// that member), read with headers(), and the door is CLAW_DOOR_URL (the
|
|
15
|
+
// app's public origin and path plus /_claw/data). No header (a render
|
|
16
|
+
// outside a member's request, or a machine whose proxy does not stamp it
|
|
17
|
+
// yet) or no CLAW_DOOR_URL: each read answers { ok: false, unavailable:
|
|
18
|
+
// true, code: "no_door" } and asks nothing. Read in the browser then.
|
|
19
|
+
//
|
|
20
|
+
// This file pulls in next/headers, so a "use client" file cannot import
|
|
21
|
+
// it (the build says so), as with lib/data.js.
|
|
22
|
+
|
|
23
|
+
const NO_DOOR = "no door grant on this request: read the brain in the browser (lib/brain.js)";
|
|
24
|
+
|
|
25
|
+
/** The bearer the proxy stamped on this request, or null. */
|
|
26
|
+
async function doorGrant() {
|
|
27
|
+
try {
|
|
28
|
+
return (await headers()).get("x-claw-door") || null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null; // outside a request there are no headers at all
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Send one read to the door with the bearer: a claw()-shaped answer. */
|
|
35
|
+
async function viaDoorGrant(path, signal) {
|
|
36
|
+
const grant = await doorGrant();
|
|
37
|
+
const door = String(process.env.CLAW_DOOR_URL ?? "").replace(/\/+$/, "");
|
|
38
|
+
if (!grant || !door) return { ok: false, status: 0, error: NO_DOOR, code: "no_door", unavailable: true };
|
|
39
|
+
let res;
|
|
40
|
+
try {
|
|
41
|
+
res = await fetch(`${door}/${path}`, {
|
|
42
|
+
cache: "no-store",
|
|
43
|
+
headers: { accept: "application/json", authorization: `Bearer ${grant}` },
|
|
44
|
+
signal,
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error?.name === "AbortError") return { ok: false, status: 0, error: "aborted", aborted: true };
|
|
48
|
+
return { ok: false, status: 0, error: "not reachable", unavailable: true };
|
|
49
|
+
}
|
|
50
|
+
return clawAnswer(res);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** → { ok, brain, kinds, error }, as lib/brain.js kinds(). */
|
|
54
|
+
export function kinds(args) {
|
|
55
|
+
return brainRead("kinds", args, viaDoorGrant);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** → { ok, records, next, error }, as lib/brain.js listRecords(). */
|
|
59
|
+
export function listRecords(args) {
|
|
60
|
+
return brainRead("listRecords", args, viaDoorGrant);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** → { ok, record, links, error }, as lib/brain.js getRecord(). */
|
|
64
|
+
export function getRecord(args) {
|
|
65
|
+
return brainRead("getRecord", args, viaDoorGrant);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** → { ok, count, by, error }, as lib/brain.js countRecords(). */
|
|
69
|
+
export function countRecords(args) {
|
|
70
|
+
return brainRead("countRecords", args, viaDoorGrant);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** → { ok, meaning, results, error }, as lib/brain.js searchBrain(). */
|
|
74
|
+
export function searchBrain(args) {
|
|
75
|
+
return brainRead("searchBrain", args, viaDoorGrant);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** → { ok, changes, next, error }, as lib/brain.js recordHistory(). */
|
|
79
|
+
export function recordHistory(args) {
|
|
80
|
+
return brainRead("recordHistory", args, viaDoorGrant);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** → { ok, changes, next, error }, as lib/brain.js brainChanges(). */
|
|
84
|
+
export function brainChanges(args) {
|
|
85
|
+
return brainRead("brainChanges", args, viaDoorGrant);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** → { ok, lists, error }, as lib/brain.js brainLists(). */
|
|
89
|
+
export function brainLists(args) {
|
|
90
|
+
return brainRead("brainLists", args, viaDoorGrant);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** → { ok, list, records, next, error }, as lib/brain.js listMembers(). */
|
|
94
|
+
export function listMembers(args) {
|
|
95
|
+
return brainRead("listMembers", args, viaDoorGrant);
|
|
96
|
+
}
|