makersclaw 0.6.0 → 0.8.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 +56 -9
- 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 +6 -6
- 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 +10 -4
- 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 +126 -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 +3 -3
- 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 +24 -12
- 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/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +47 -14
- package/registry/blocks/chat/demo.jsx +2 -2
- 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 +30 -12
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +7 -4
- 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 +20 -7
- package/registry/blocks/org-chart/README.md +14 -9
- package/registry/blocks/page-builder/README.md +14 -7
- package/registry/blocks/page-builder/demo.jsx +3 -3
- 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 +26 -21
- 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/sidebar/README.md +23 -13
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +25 -10
- 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 +34 -22
- package/src/cli.mjs +33 -6
- package/src/install.mjs +285 -26
- package/src/registry.mjs +109 -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 +1100 -89
- package/templates/next/files/app/frame.js +128 -4
- package/templates/next/files/app/shell.jsx +54 -37
- package/templates/next/files/check/run.mjs +1514 -45
- package/templates/next/files/components/ui/index.js +1 -0
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/layout.jsx +307 -0
- 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 +15 -1
- 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/lib/hooks.js +71 -2
- package/templates/next/files/lib/platform.js +93 -4
- package/templates/next/files/next.config.mjs +36 -2
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/template.json +18 -3
- package/templates/next/files/schema.sql +0 -22
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { headers } from "next/headers";
|
|
2
|
-
import {
|
|
2
|
+
import { ident, query } from "./db";
|
|
3
3
|
import { markSample } from "./sample";
|
|
4
4
|
|
|
5
5
|
// SERVER reads. Import from server components and server actions only —
|
|
@@ -38,21 +38,111 @@ export function plain(row) {
|
|
|
38
38
|
return out;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
41
|
+
// A filter is the platform data door's grammar (claw's STATIC-SPEC §1,
|
|
42
|
+
// "The routes"), so a surface's filter means the same in a server app,
|
|
43
|
+
// read here, and in a browser one, read by the door. Up to 0.6.x it knew
|
|
44
|
+
// equals, lists and null, and any other object went to pg as a value.
|
|
45
|
+
const WHERE_MAX = 20;
|
|
46
|
+
const LIST_MAX = 1000;
|
|
47
|
+
const BOUNDS = [
|
|
48
|
+
["gt", ">"],
|
|
49
|
+
["gte", ">="],
|
|
50
|
+
["lt", "<"],
|
|
51
|
+
["lte", "<="],
|
|
52
|
+
];
|
|
53
|
+
const GRAMMAR = "takes a value, [values], null, or one of {not}, {gt|gte|lt|lte}, {has}, {prefix}";
|
|
54
|
+
|
|
55
|
+
/** A value a condition compares with: text, a finite number, true or
|
|
56
|
+
* false, or a Date (JSON sends it as its ISO time). */
|
|
57
|
+
function isValue(v) {
|
|
58
|
+
if (typeof v === "string" || typeof v === "boolean") return true;
|
|
59
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
60
|
+
return v instanceof Date && !Number.isNaN(v.getTime());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A prefix for LIKE: lower-cased, LIKE's own marks escaped (Postgres's
|
|
64
|
+
* default escape is a backslash), then "%". */
|
|
65
|
+
function likePrefix(text) {
|
|
66
|
+
return `${text.toLowerCase().replace(/[\\%_]/g, (m) => `\\${m}`)}%`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One `column: condition` as SQL; `bind` adds a value and names it. */
|
|
70
|
+
function condition(name, cond, bind) {
|
|
71
|
+
const refuse = (what) => {
|
|
72
|
+
throw new Error(`filter: ${name} ${what}`);
|
|
73
|
+
};
|
|
74
|
+
if (name.includes(".")) refuse("is not a column: name the column alone");
|
|
75
|
+
const col = ident(name);
|
|
76
|
+
if (cond === null) return `${col} IS NULL`;
|
|
77
|
+
if (isValue(cond)) return `${col} = ${bind(cond)}`;
|
|
78
|
+
if (Array.isArray(cond)) {
|
|
79
|
+
if (cond.length > LIST_MAX) refuse(`takes at most ${LIST_MAX.toLocaleString("en-US")} values in a list`);
|
|
80
|
+
if (!cond.every(isValue)) refuse("takes a list of values (text, numbers, true or false): filter empty ones with null");
|
|
81
|
+
return `${col} = ANY(${bind(cond)})`;
|
|
82
|
+
}
|
|
83
|
+
if (typeof cond !== "object" || cond instanceof Date) refuse(GRAMMAR);
|
|
84
|
+
// A key left undefined is not there, as JSON leaves it out on its way
|
|
85
|
+
// to the door: { gte: from, lte: to } with no `to` is one bound.
|
|
86
|
+
const keys = Object.keys(cond).filter((k) => cond[k] !== undefined);
|
|
87
|
+
if (keys.length === 1 && keys[0] === "not") {
|
|
88
|
+
if (cond.not === null) return `${col} IS NOT NULL`;
|
|
89
|
+
if (!isValue(cond.not)) refuse("takes {not: value} or {not: null}");
|
|
90
|
+
// Distinct, not <>: a row with no value is not the value either.
|
|
91
|
+
return `${col} IS DISTINCT FROM ${bind(cond.not)}`;
|
|
92
|
+
}
|
|
93
|
+
if (keys.length === 1 && keys[0] === "has") {
|
|
94
|
+
if (!isValue(cond.has)) refuse("takes {has: value}");
|
|
95
|
+
// An array column holds it. (The door also reads a jsonb column's
|
|
96
|
+
// `?`; it knows the column's type, which a filter here does not.)
|
|
97
|
+
return `${bind(cond.has)} = ANY(${col})`;
|
|
98
|
+
}
|
|
99
|
+
if (keys.length === 1 && keys[0] === "prefix") {
|
|
100
|
+
if (typeof cond.prefix !== "string" || !cond.prefix) refuse('wants {prefix: "some text"}');
|
|
101
|
+
return `lower(${col}) LIKE ${bind(likePrefix(cond.prefix))}`;
|
|
102
|
+
}
|
|
103
|
+
const bounds = BOUNDS.map(([k]) => k);
|
|
104
|
+
if (keys.length && keys.every((k) => bounds.includes(k))) {
|
|
105
|
+
const lower = keys.filter((k) => k === "gt" || k === "gte");
|
|
106
|
+
const upper = keys.filter((k) => k === "lt" || k === "lte");
|
|
107
|
+
if (lower.length > 1 || upper.length > 1) refuse("takes one lower bound (gt or gte) and one upper (lt or lte)");
|
|
108
|
+
const parts = [];
|
|
109
|
+
for (const [k, op] of BOUNDS) {
|
|
110
|
+
if (!keys.includes(k)) continue;
|
|
111
|
+
if (!isValue(cond[k])) refuse(`takes {${k}: value}: a number, a day or a time`);
|
|
112
|
+
parts.push(`${col} ${op} ${bind(cond[k])}`);
|
|
54
113
|
}
|
|
114
|
+
return parts.join(" AND ");
|
|
115
|
+
}
|
|
116
|
+
return refuse(GRAMMAR);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `filter` → " WHERE …" text, "" for none; `params` is filled in place and
|
|
121
|
+
* every value is bound. An AND of `{ column: condition }`, at most 20:
|
|
122
|
+
*
|
|
123
|
+
* value equals
|
|
124
|
+
* [value, …] one of (at most 1,000)
|
|
125
|
+
* null empty (IS NULL)
|
|
126
|
+
* { not: value | null } anything else (IS DISTINCT FROM), or not empty
|
|
127
|
+
* { gt | gte | lt | lte: v } a bound; one lower and one upper may combine
|
|
128
|
+
* { has: value } an array column holds it
|
|
129
|
+
* { prefix: "text" } starts with, case ignored
|
|
130
|
+
*
|
|
131
|
+
* A column whose condition is undefined is left out, as JSON leaves it
|
|
132
|
+
* out. Anything else throws, and the read returns it as its error.
|
|
133
|
+
*/
|
|
134
|
+
export function whereClause(filter, params) {
|
|
135
|
+
if (filter === undefined || filter === null) return "";
|
|
136
|
+
if (typeof filter !== "object" || Array.isArray(filter)) {
|
|
137
|
+
throw new Error('filter is an object of column names to conditions, like { stage: "won" }');
|
|
55
138
|
}
|
|
139
|
+
const entries = Object.entries(filter).filter(([, cond]) => cond !== undefined);
|
|
140
|
+
if (entries.length > WHERE_MAX) throw new Error(`filter holds at most ${WHERE_MAX} conditions`);
|
|
141
|
+
const bind = (value) => {
|
|
142
|
+
params.push(value);
|
|
143
|
+
return `$${params.length}`;
|
|
144
|
+
};
|
|
145
|
+
const where = entries.map(([name, cond]) => condition(name, cond, bind));
|
|
56
146
|
return where.length ? ` WHERE ${where.join(" AND ")}` : "";
|
|
57
147
|
}
|
|
58
148
|
|
|
@@ -63,7 +153,8 @@ function failed(what, error) {
|
|
|
63
153
|
}
|
|
64
154
|
|
|
65
155
|
/**
|
|
66
|
-
* Rows of a source table. `filter` is
|
|
156
|
+
* Rows of a source table. `filter` is whereClause's grammar (above:
|
|
157
|
+
* { column: condition }, the data door's); `orderBy` is
|
|
67
158
|
* "updated_at desc" (default: idColumn desc); `limit` caps at 5000.
|
|
68
159
|
* → { rows, error }
|
|
69
160
|
*/
|
|
@@ -121,37 +212,36 @@ export async function countBy({ source, column, filter }) {
|
|
|
121
212
|
}
|
|
122
213
|
|
|
123
214
|
/**
|
|
124
|
-
* What happened, newest first:
|
|
215
|
+
* What happened, newest first: this app's lines in claw_app.activity
|
|
125
216
|
* (every lib/actions.js write and everything the agent recorded with the
|
|
126
|
-
* sql verb).
|
|
127
|
-
*
|
|
128
|
-
*
|
|
217
|
+
* sql verb). The view holds only this app's lines, so nothing here names
|
|
218
|
+
* the app. `surface` narrows to one surface key; `since` (ISO or Date) to
|
|
219
|
+
* rows after that time. Rows: { id, at, action, surface, row_ref, detail,
|
|
220
|
+
* data, by }. Outside Makersclaw there is no such view: `error` says so.
|
|
221
|
+
* → { rows, error }
|
|
129
222
|
*/
|
|
130
223
|
export async function loadActivity({ limit = 100, surface, since } = {}) {
|
|
131
|
-
const id = artifactId();
|
|
132
|
-
if (!id) {
|
|
133
|
-
return { rows: [], error: failed("public.artifact_activity", "no CLAW_ARTIFACT_ID; product_run sets it") };
|
|
134
|
-
}
|
|
135
224
|
try {
|
|
136
|
-
const params = [
|
|
137
|
-
|
|
225
|
+
const params = [];
|
|
226
|
+
const where = [];
|
|
138
227
|
if (surface) {
|
|
139
228
|
params.push(String(surface));
|
|
140
|
-
where
|
|
229
|
+
where.push(`surface = $${params.length}`);
|
|
141
230
|
}
|
|
142
231
|
if (since) {
|
|
143
232
|
params.push(since instanceof Date ? since.toISOString() : String(since));
|
|
144
|
-
where
|
|
233
|
+
where.push(`at > $${params.length}::timestamptz`);
|
|
145
234
|
}
|
|
146
235
|
params.push(Math.max(1, Math.min(1000, Number(limit) || 100)));
|
|
236
|
+
const filter = where.length ? ` WHERE ${where.join(" AND ")}` : "";
|
|
147
237
|
const { rows } = await query(
|
|
148
238
|
`SELECT id, at, action, surface, row_ref, detail, data, by
|
|
149
|
-
FROM
|
|
239
|
+
FROM claw_app.activity${filter} ORDER BY at DESC, id DESC LIMIT $${params.length}`,
|
|
150
240
|
params,
|
|
151
241
|
);
|
|
152
242
|
return { rows: rows.map(plain), error: null };
|
|
153
243
|
} catch (error) {
|
|
154
|
-
return { rows: [], error: failed("
|
|
244
|
+
return { rows: [], error: failed("claw_app.activity", error) };
|
|
155
245
|
}
|
|
156
246
|
}
|
|
157
247
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { headers } from "next/headers";
|
|
1
2
|
import { Pool, types } from "pg";
|
|
2
3
|
|
|
3
4
|
// A `date` column is a day on a wall calendar, not an instant, so it is
|
|
@@ -26,18 +27,84 @@ globalThis._pgPool = pool;
|
|
|
26
27
|
|
|
27
28
|
export const query = (text, params) => pool.query(text, params);
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Who is asking: the member the platform's proxy named on this request
|
|
32
|
+
* (`x-claw-user-id`, `x-claw-user-email`; the proxy drops a browser's own
|
|
33
|
+
* copies first, so they are only ever its word). `''` for each one it did
|
|
34
|
+
* not name: a local run, a render outside a request, a job.
|
|
35
|
+
* → { id, email }
|
|
36
|
+
*/
|
|
37
|
+
export async function requestMember() {
|
|
38
|
+
try {
|
|
39
|
+
const asked = await headers();
|
|
40
|
+
return { id: asked.get("x-claw-user-id") || "", email: asked.get("x-claw-user-email") || "" };
|
|
41
|
+
} catch {
|
|
42
|
+
return { id: "", email: "" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The member, told to the database inside the write's own transaction. The
|
|
47
|
+
// third argument, true, makes each setting local: the COMMIT ends it, so the
|
|
48
|
+
// next statement on this pooled connection never runs as this member. Never
|
|
49
|
+
// false (the platform's rule since the Neon probe; test/data.test.mjs greps
|
|
50
|
+
// the template for it). Values are bound, never written into the text.
|
|
51
|
+
const MEMBER_SETTINGS =
|
|
52
|
+
"SELECT set_config('claw.member_id', $1, true), set_config('claw.member_email', $2, true)";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run `work(query)` in one transaction on one connection, after telling
|
|
56
|
+
* the database who is asking. A default, a trigger or an action function
|
|
57
|
+
* reads it the same way in a server app and a browser one (where the
|
|
58
|
+
* platform's data door sets the same two):
|
|
59
|
+
*
|
|
60
|
+
* nullif(current_setting('claw.member_email', true), '')
|
|
61
|
+
*
|
|
62
|
+
* It is a label, not a permission: nothing in the platform trusts it, and
|
|
63
|
+
* a column the page must not choose (`created_by`) is set by a trigger
|
|
64
|
+
* that reads it. Every lib/actions.js write runs here. `work` gets a
|
|
65
|
+
* `query(text, params)` bound to the transaction's connection; use only
|
|
66
|
+
* that inside it. → whatever `work` returns. A failure rolls back and
|
|
67
|
+
* throws on.
|
|
68
|
+
*/
|
|
69
|
+
export async function transaction(work) {
|
|
70
|
+
const member = await requestMember();
|
|
71
|
+
const client = await pool.connect();
|
|
72
|
+
let broken;
|
|
73
|
+
try {
|
|
74
|
+
await client.query("BEGIN");
|
|
75
|
+
await client.query(MEMBER_SETTINGS, [member.id, member.email]);
|
|
76
|
+
const result = await work((text, params) => client.query(text, params));
|
|
77
|
+
await client.query("COMMIT");
|
|
78
|
+
return result;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
try {
|
|
81
|
+
await client.query("ROLLBACK");
|
|
82
|
+
} catch (lost) {
|
|
83
|
+
// The connection itself failed: the pool must not lend it again.
|
|
84
|
+
broken = lost;
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
} finally {
|
|
88
|
+
client.release(broken);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
29
92
|
/** The artifact this app belongs to. product_run sets it from the folder
|
|
30
|
-
* the app runs in;
|
|
31
|
-
*
|
|
93
|
+
* the app runs in; app/layout.jsx reads it to turn examples off, and an
|
|
94
|
+
* app may use it to name itself. The activity lines no longer need it:
|
|
95
|
+
* claw_app.activity fills in the app itself (logActivity, below). */
|
|
32
96
|
export function artifactId() {
|
|
33
97
|
return process.env.CLAW_ARTIFACT_ID || null;
|
|
34
98
|
}
|
|
35
99
|
|
|
36
100
|
const IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
37
101
|
|
|
38
|
-
/** Quote a table or column name for SQL: "investors" → "investors"
|
|
39
|
-
*
|
|
40
|
-
*
|
|
102
|
+
/** Quote a table or column name for SQL: "investors" → "investors".
|
|
103
|
+
* An app names its tables bare: its own schema is first on its role's
|
|
104
|
+
* search_path. Two parts still pass ("public.investors" →
|
|
105
|
+
* "public"."investors"), for an app from before 0.7.0 whose tables are
|
|
106
|
+
* in public and whose code says so. Anything else throws, so a typo in a
|
|
107
|
+
* surface config can never reach the database as SQL. */
|
|
41
108
|
export function ident(name) {
|
|
42
109
|
const parts = String(name ?? "").split(".");
|
|
43
110
|
if (!parts.length || parts.length > 2 || !parts.every((p) => IDENT.test(p))) {
|
|
@@ -46,13 +113,26 @@ export function ident(name) {
|
|
|
46
113
|
return parts.map((p) => `"${p}"`).join(".");
|
|
47
114
|
}
|
|
48
115
|
|
|
116
|
+
// Postgres's "relation does not exist": no claw_app.activity to write to.
|
|
117
|
+
const NO_SUCH_RELATION = "42P01";
|
|
118
|
+
|
|
49
119
|
/**
|
|
50
120
|
* Record what happened, for the Activity tab the OS draws on the app
|
|
51
|
-
* window (and any surface that reads the
|
|
52
|
-
* lib/actions.js write calls this; the
|
|
53
|
-
*
|
|
121
|
+
* window (and any surface that reads the lines with loadActivity). Every
|
|
122
|
+
* lib/actions.js write calls this; the app's agent writes the same lines
|
|
123
|
+
* with the sql verb.
|
|
54
124
|
* Never throws — an activity line must not fail the write it describes.
|
|
55
125
|
*
|
|
126
|
+
* The line goes into claw_app.activity: the platform's view of the
|
|
127
|
+
* workspace's activity table, holding only this app's lines. The view
|
|
128
|
+
* fills in `artifact_id` (the app whose role the connection runs as) and
|
|
129
|
+
* `id`, so this names neither: the app needs no CLAW_ARTIFACT_ID, and it
|
|
130
|
+
* cannot write a line as another app. Up to 0.6.x it inserted into
|
|
131
|
+
* public.artifact_activity with CLAW_ARTIFACT_ID; an app on its own schema
|
|
132
|
+
* can reach nothing in public. Outside Makersclaw, or on a platform from
|
|
133
|
+
* before the view, there is no claw_app.activity: the line is not
|
|
134
|
+
* recorded, and this returns false, as on any other failure.
|
|
135
|
+
*
|
|
56
136
|
* action found, drafted, sent, replied, skipped, edited, moved, added…
|
|
57
137
|
* surface the surface key the row belongs to (the OS tab shows its
|
|
58
138
|
* label; a link into the app is /s/<surface>?row=<row_ref>)
|
|
@@ -62,17 +142,11 @@ export function ident(name) {
|
|
|
62
142
|
* by 'agent' | 'user:<email>' | 'run:<id>'
|
|
63
143
|
*/
|
|
64
144
|
export async function logActivity({ action, surface, row_ref, detail, data, by }) {
|
|
65
|
-
const id = artifactId();
|
|
66
|
-
if (!id) {
|
|
67
|
-
console.warn(`[activity] not recorded (no CLAW_ARTIFACT_ID): ${action} ${detail ?? ""}`);
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
145
|
try {
|
|
71
146
|
await query(
|
|
72
|
-
`INSERT INTO
|
|
73
|
-
VALUES ($1, $2, $3, $4, $5
|
|
147
|
+
`INSERT INTO claw_app.activity (action, surface, row_ref, detail, data, by)
|
|
148
|
+
VALUES ($1, $2, $3, $4, $5::jsonb, $6)`,
|
|
74
149
|
[
|
|
75
|
-
id,
|
|
76
150
|
String(action || "edited"),
|
|
77
151
|
surface ?? null,
|
|
78
152
|
row_ref == null ? null : String(row_ref),
|
|
@@ -83,7 +157,13 @@ export async function logActivity({ action, surface, row_ref, detail, data, by }
|
|
|
83
157
|
);
|
|
84
158
|
return true;
|
|
85
159
|
} catch (error) {
|
|
86
|
-
|
|
160
|
+
// No view is where the app runs, not a fault in it: a warning, as the
|
|
161
|
+
// missing CLAW_ARTIFACT_ID was up to 0.6.x. Anything else is an error.
|
|
162
|
+
if (error?.code === NO_SUCH_RELATION) {
|
|
163
|
+
console.warn(`[activity] not recorded (no claw_app.activity here): ${action} ${detail ?? ""}`);
|
|
164
|
+
} else {
|
|
165
|
+
console.error(`[activity] not recorded: ${String(error)}`);
|
|
166
|
+
}
|
|
87
167
|
return false;
|
|
88
168
|
}
|
|
89
169
|
}
|