@lotics/app-sdk 0.119.0 → 0.120.1
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/AGENTS.md +1 -1
- package/dist/{chunk-UFWVZOJE.js → chunk-NZXJENJ2.js} +25 -7
- package/dist/index.d.ts +0 -3
- package/dist/index.js +88 -108
- package/dist/router.js +1 -1
- package/dist/rpc.d.ts +1 -1
- package/dist/shared_types.d.ts +7 -5
- package/docs/data_fetching.md +1 -1
- package/docs/members_and_options.md +0 -40
- package/docs/mutations.md +4 -4
- package/docs/queries.md +24 -3
- package/docs/security.md +0 -2
- package/package.json +1 -1
- package/dist/imports.d.ts +0 -51
package/AGENTS.md
CHANGED
|
@@ -20,7 +20,7 @@ This file is the index. The **exact type** of anything is its shipped declaratio
|
|
|
20
20
|
| [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY reference** — the JS subset a body may use, opaque `fld_*`/`opt_*` keys, every step form, the accepted sugar, helpers, record-write surfaces, the traps and the verify loop. |
|
|
21
21
|
| [docs/recipes.md](./docs/recipes.md) | Task-shaped how-tos — returning a generated file, returning structured data, parameterized lookups, composable optional filters, cell decoding, testing an AI action without spending credits. |
|
|
22
22
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload` and its `fidelity` and a photo's `place`, `renameFile` (a new file over the same bytes), `useAttachments`/`useAttachmentPiles`, `readFiles` and presigned URLs (a bearer credential — never logged or persisted), workflow-generated files, naming a zip's entries, the delivery bounds. |
|
|
23
|
-
| [docs/members_and_options.md](./docs/members_and_options.md) | People, select options and comments — `useMembers`, `useFieldOptions`, `useViewer`, `useWorkspaceTimezone`/`useWorkspaceCurrency`, `useAppContext`, the workspace's zone at the root, `useComments`, a record's activity (`useRecordActivity`)
|
|
23
|
+
| [docs/members_and_options.md](./docs/members_and_options.md) | People, select options and comments — `useMembers`, `useFieldOptions`, `useViewer`, `useWorkspaceTimezone`/`useWorkspaceCurrency`, `useAppContext`, the workspace's zone at the root, `useComments`, a record's activity (`useRecordActivity`). |
|
|
24
24
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model, the screens and `drawers` it reports to the host), `useUrlState` + `urlParam` codecs, `useRecents`, `useFolderPick`. |
|
|
25
25
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming parts, the agent's ask-back), `askAi`, `useAiContext`, and what the member's own chat agent can do with the app while it is open. |
|
|
26
26
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, why a per-input bound is a tenancy floor rather than an authorization check. |
|
|
@@ -174,6 +174,17 @@ function transportErrorMessage(status, parsed) {
|
|
|
174
174
|
return status < 500 || authored ? jsonMessage : gatewayErrorMessage(status);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// ../shared/src/schemas/app_spec_rules.ts
|
|
178
|
+
function tiersAscend(tiers) {
|
|
179
|
+
return tiers.every((tier, index) => index === 0 || tier > (tiers[index - 1] ?? tier));
|
|
180
|
+
}
|
|
181
|
+
var bandKey = (band) => `${band.min ?? ""}..${band.max ?? ""}`;
|
|
182
|
+
function bandOf(tiers, figure) {
|
|
183
|
+
const above = tiers.findIndex((tier) => figure < tier);
|
|
184
|
+
const at = above < 0 ? tiers.length : above;
|
|
185
|
+
return { min: tiers[at - 1] ?? null, max: tiers[at] ?? null };
|
|
186
|
+
}
|
|
187
|
+
|
|
177
188
|
// ../shared/src/utils.ts
|
|
178
189
|
function decimalPlaces(value) {
|
|
179
190
|
if (Number.isInteger(value)) return 0;
|
|
@@ -213,12 +224,19 @@ function aggregateRows(rows, aggregate) {
|
|
|
213
224
|
const by = aggregate.by ?? [];
|
|
214
225
|
const day = aggregate.day;
|
|
215
226
|
const sum = aggregate.sum;
|
|
227
|
+
const keyOf = (row, column) => {
|
|
228
|
+
const tiers = aggregate.bands?.[column];
|
|
229
|
+
const cell = row[column] ?? null;
|
|
230
|
+
if (tiers === void 0) return cell;
|
|
231
|
+
return typeof cell === "number" ? bandKey(bandOf(tiers, cell)) : null;
|
|
232
|
+
};
|
|
216
233
|
const keyed = /* @__PURE__ */ new Map();
|
|
217
234
|
for (const row of rows) {
|
|
218
|
-
const
|
|
219
|
-
|
|
235
|
+
const keys = by.map((column) => keyOf(row, column));
|
|
236
|
+
const key = JSON.stringify([...keys, ...day === void 0 ? [] : [dayOf(row[day])]]);
|
|
237
|
+
keyed.set(key, { keys, members: [...keyed.get(key)?.members ?? [], row] });
|
|
220
238
|
}
|
|
221
|
-
return [...keyed.values()].map((members) => {
|
|
239
|
+
return [...keyed.values()].map(({ keys, members }) => {
|
|
222
240
|
const [first2] = members;
|
|
223
241
|
const figures = members.flatMap((member) => {
|
|
224
242
|
const value = sum === void 0 ? void 0 : member[sum];
|
|
@@ -226,7 +244,7 @@ function aggregateRows(rows, aggregate) {
|
|
|
226
244
|
});
|
|
227
245
|
return {
|
|
228
246
|
[RECORD_ID_COLUMN]: null,
|
|
229
|
-
...Object.fromEntries(by.map((column) => [column,
|
|
247
|
+
...Object.fromEntries(by.map((column, at) => [column, keys[at] ?? null])),
|
|
230
248
|
...day === void 0 ? {} : { [AGGREGATE_DAY_COLUMN]: dayOf(first2?.[day]) },
|
|
231
249
|
[AGGREGATE_COUNT_COLUMN]: members.length,
|
|
232
250
|
...sum === void 0 ? {} : { [AGGREGATE_SUM_COLUMN]: figures.length === 0 ? null : sumDecimal(figures) }
|
|
@@ -1016,9 +1034,6 @@ function rpcStandalone(op, payload) {
|
|
|
1016
1034
|
return rejectCommentsStandalone();
|
|
1017
1035
|
case "activity.list":
|
|
1018
1036
|
return Promise.reject(new Error("A record's activity is available only in embedded apps \u2014 a signed-in member is required."));
|
|
1019
|
-
case "imports.list":
|
|
1020
|
-
case "imports.run":
|
|
1021
|
-
return Promise.reject(new Error("A register's imports log is available only in embedded apps \u2014 a signed-in member is required."));
|
|
1022
1037
|
case "recording.start":
|
|
1023
1038
|
case "recording.stop":
|
|
1024
1039
|
return Promise.reject(
|
|
@@ -1224,6 +1239,9 @@ async function standaloneUpload(file, fidelity, place) {
|
|
|
1224
1239
|
|
|
1225
1240
|
export {
|
|
1226
1241
|
__export,
|
|
1242
|
+
tiersAscend,
|
|
1243
|
+
bandKey,
|
|
1244
|
+
bandOf,
|
|
1227
1245
|
sumDecimal,
|
|
1228
1246
|
sample,
|
|
1229
1247
|
assertNever,
|
package/dist/index.d.ts
CHANGED
|
@@ -13,9 +13,6 @@ export { useComments, useCommentCounts } from "./comments.js";
|
|
|
13
13
|
export type { AppComment, AppCommentAuthor, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
|
|
14
14
|
export { useRecordActivity } from "./activity.js";
|
|
15
15
|
export type { RecordActivityOptions, RecordActivityState } from "./activity.js";
|
|
16
|
-
export { useImportRun, useImportsLog } from "./imports.js";
|
|
17
|
-
export type { ImportRunState, ImportsLogState, ImportsPageOptions } from "./imports.js";
|
|
18
|
-
export type { ImportRow, ImportRowKind, ImportRun, ImportRunEnd, ImportRunPage, ImportRunStop, ImportsLogPage, ImportWorkflow } from "./shared_types.js";
|
|
19
16
|
export type { ActivityActor, ActivityActorKind, ActivityChange, ActivityComment, ActivityEntry, ActivityValue, RecordActivityPage, } from "./shared_types.js";
|
|
20
17
|
export { useAppContext, useViewer, useWorkspaceCurrency, useWorkspaceTimezone } from "./viewer.js";
|
|
21
18
|
export type { AppAbsence } from "./shared_types.js";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
anyRecordingLive,
|
|
8
8
|
applyPatch,
|
|
9
9
|
assertNever,
|
|
10
|
+
bandKey,
|
|
11
|
+
bandOf,
|
|
10
12
|
decodeAll,
|
|
11
13
|
encodePatch,
|
|
12
14
|
getUrlParams,
|
|
@@ -27,8 +29,9 @@ import {
|
|
|
27
29
|
subscribeRecordings,
|
|
28
30
|
subscribeUrlParams,
|
|
29
31
|
sumDecimal,
|
|
32
|
+
tiersAscend,
|
|
30
33
|
urlParam
|
|
31
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-NZXJENJ2.js";
|
|
32
35
|
|
|
33
36
|
// src/mount.tsx
|
|
34
37
|
import { createRoot } from "react-dom/client";
|
|
@@ -5944,11 +5947,10 @@ function parseExpression(expression) {
|
|
|
5944
5947
|
return parseWholeExpression(expression, astFactory);
|
|
5945
5948
|
}
|
|
5946
5949
|
function evaluateJsExpression(expression, data, additionalFunctions, defaultTimezone) {
|
|
5947
|
-
const timezoneAwareFunctions = createTimezoneAwareFunctions(defaultTimezone);
|
|
5948
5950
|
const expr2 = parseWholeExpression(expression, astFactory);
|
|
5949
5951
|
const result = expr2?.evaluate({
|
|
5950
5952
|
...data,
|
|
5951
|
-
...timezoneAwareFunctions,
|
|
5953
|
+
...timezoneAwareFunctions(defaultTimezone),
|
|
5952
5954
|
...additionalFunctions
|
|
5953
5955
|
});
|
|
5954
5956
|
return result;
|
|
@@ -6105,6 +6107,16 @@ function createTimezoneAwareFunctions(defaultTimezone) {
|
|
|
6105
6107
|
...expression_type_exports
|
|
6106
6108
|
};
|
|
6107
6109
|
}
|
|
6110
|
+
var MAX_HELD_TIMEZONES = 512;
|
|
6111
|
+
var functionsByTimezone = /* @__PURE__ */ new Map();
|
|
6112
|
+
function timezoneAwareFunctions(defaultTimezone) {
|
|
6113
|
+
const held2 = functionsByTimezone.get(defaultTimezone);
|
|
6114
|
+
if (held2 !== void 0) return held2;
|
|
6115
|
+
if (functionsByTimezone.size >= MAX_HELD_TIMEZONES) functionsByTimezone.clear();
|
|
6116
|
+
const built = Object.freeze(createTimezoneAwareFunctions(defaultTimezone));
|
|
6117
|
+
functionsByTimezone.set(defaultTimezone, built);
|
|
6118
|
+
return built;
|
|
6119
|
+
}
|
|
6108
6120
|
var numericAlphabet = customAlphabet("0123456789");
|
|
6109
6121
|
function assertRandomLength(helper, length2) {
|
|
6110
6122
|
if (length2 > MAX_RANDOM_LENGTH) {
|
|
@@ -25231,9 +25243,6 @@ var workflowSchema = zod_default.object({
|
|
|
25231
25243
|
metadata: workflowMetadataSchema.nullable().optional().describe("Metadata for the workflow"),
|
|
25232
25244
|
current_version: zod_default.number().int().describe("Current version number"),
|
|
25233
25245
|
enabled: zod_default.boolean().describe("Whether this workflow is active"),
|
|
25234
|
-
imports_into: zod_default.string().nullable().optional().describe(
|
|
25235
|
-
"The table this workflow brings data into: its runs are the imports log of a register over that table that keeps one (`register.imports`). Null, it imports into none."
|
|
25236
|
-
),
|
|
25237
25246
|
workspace_id: zod_default.string().describe("ID of the workspace this workflow belongs to"),
|
|
25238
25247
|
created_at: zod_default.string().describe("Timestamp when workflow was created"),
|
|
25239
25248
|
updated_at: zod_default.string().describe("Timestamp of last update")
|
|
@@ -25973,7 +25982,7 @@ var queryFromTableNodeSchema = zod_default.object({
|
|
|
25973
25982
|
table_id: zod_default.string().describe("Source table ID"),
|
|
25974
25983
|
filter: tableRecordFiltersSchema.optional(),
|
|
25975
25984
|
search: zod_default.string().optional().describe(
|
|
25976
|
-
"Free-text search across the record's
|
|
25985
|
+
"Free-text search across the record's text: every term must appear, as a substring, diacritics- and case-insensitive. It matches ALL text-bearing fields of the row, not just the query's projected columns \u2014 bound search-bearing queries accordingly. AND-ed with `filter`. Templatable with `{{params.x}}`. Prefer this over an OR-group of per-field `contains` for a search box \u2014 it can't silently miss a field and matches accented text."
|
|
25977
25986
|
),
|
|
25978
25987
|
sort: tableRecordSortSchema.optional(),
|
|
25979
25988
|
limit: zod_default.number().optional()
|
|
@@ -26008,12 +26017,20 @@ var queryGroupBucketKeySchema = zod_default.object({
|
|
|
26008
26017
|
output: zod_default.string().describe("Output column name for the bucket.")
|
|
26009
26018
|
})
|
|
26010
26019
|
});
|
|
26011
|
-
var
|
|
26020
|
+
var bandTiersSchema = zod_default.array(zod_default.number()).min(1).refine(tiersAscend, "Breakpoints ascend, each above the one before it.");
|
|
26021
|
+
var queryGroupBandKeySchema = zod_default.object({
|
|
26022
|
+
band: zod_default.object({
|
|
26023
|
+
source: zod_default.string().describe("Input number column to band."),
|
|
26024
|
+
tiers: bandTiersSchema.describe("Ascending breakpoints. A value on one is in the band above it."),
|
|
26025
|
+
output: zod_default.string().describe("Output column name for the band's key.")
|
|
26026
|
+
})
|
|
26027
|
+
});
|
|
26028
|
+
var queryGroupKeySchema = zod_default.union([zod_default.string(), queryGroupBucketKeySchema, queryGroupBandKeySchema]);
|
|
26012
26029
|
var queryGroupNodeSchema = zod_default.object({
|
|
26013
26030
|
kind: zod_default.literal("group"),
|
|
26014
26031
|
from: zod_default.lazy(() => queryNodeSchema),
|
|
26015
26032
|
by: zod_default.array(queryGroupKeySchema).describe(
|
|
26016
|
-
"Group keys: input column names,
|
|
26033
|
+
"Group keys: input column names, `{ bucket: { source, granularity, output } }` date buckets, or `{ band: { source, tiers, output } }` bands of a number. A bucket truncates a date/datetime column to the bucket START as a `date` column (wall-clock semantics; `week` starts on ISO Monday; null source \u2192 null bucket). A band answers a text column holding `from..to`, the two breakpoints the value stands between (the lower held, the upper not; an open end empty: `..10`, `10..20`, `20..`); no number \u2192 null band. Empty = single-row aggregate."
|
|
26017
26034
|
),
|
|
26018
26035
|
sets: zod_default.array(zod_default.array(zod_default.string())).min(1).optional().describe(
|
|
26019
26036
|
"Several groupings answered by one read: each lists the `by` outputs it groups by (`[]` = one row over every row). A key outside a row's set is null there, and `__grouping` holds the set's bits: one per `by` key it leaves out, the last key the lowest. Absent: one grouping by every key."
|
|
@@ -31333,6 +31350,13 @@ var HELPERS2 = new Set(Object.keys(createTimezoneAwareFunctions()));
|
|
|
31333
31350
|
var BY_LOWERCASE = new Map([...HELPERS2].map((name) => [name.toLowerCase(), name]));
|
|
31334
31351
|
|
|
31335
31352
|
// ../shared/src/formula.ts
|
|
31353
|
+
function parseOnce(evaluable) {
|
|
31354
|
+
try {
|
|
31355
|
+
return { kind: "parsed", expression: parseExpression(evaluable) };
|
|
31356
|
+
} catch (error51) {
|
|
31357
|
+
return { kind: "unparsable", error: error51 };
|
|
31358
|
+
}
|
|
31359
|
+
}
|
|
31336
31360
|
function prepareFormulaContext(fields) {
|
|
31337
31361
|
const formulaFields = fields.filter((field) => field.type === "formula");
|
|
31338
31362
|
const { sortedFields, circularDependencies } = buildDependencyGraph(formulaFields, fields);
|
|
@@ -31368,8 +31392,9 @@ function prepareFormulaContext(fields) {
|
|
|
31368
31392
|
function computeFormulaFieldsWithContext(row2, fields, context, linkedTableRecords = /* @__PURE__ */ new Map(), linkedTableFields = /* @__PURE__ */ new Map(), timezone, loadedLinkKeys, linkExpandedTableIds, fileFacts, memberNames) {
|
|
31369
31393
|
const result = { ...row2 };
|
|
31370
31394
|
const mayRecompute = (linkKey) => loadedLinkKeys === void 0 || loadedLinkKeys.has(linkKey) || isEmptyFieldValue(row2[linkKey]);
|
|
31395
|
+
const computes = (key) => context.computedAggregates === void 0 || context.computedAggregates.has(key);
|
|
31371
31396
|
for (const { field, sourceField } of context.rollupFields) {
|
|
31372
|
-
if (!mayRecompute(sourceField.key)) continue;
|
|
31397
|
+
if (!computes(field.key) || !mayRecompute(sourceField.key)) continue;
|
|
31373
31398
|
if (linkedTableRecords.has(sourceField.table_id) && linkedTableFields.has(sourceField.table_id)) {
|
|
31374
31399
|
const linkedRecords = linkedTableRecords.get(sourceField.table_id) ?? [];
|
|
31375
31400
|
const linkedTableFieldsArray = linkedTableFields.get(sourceField.table_id) ?? [];
|
|
@@ -31419,7 +31444,7 @@ function computeFormulaFieldsWithContext(row2, fields, context, linkedTableRecor
|
|
|
31419
31444
|
}
|
|
31420
31445
|
}
|
|
31421
31446
|
for (const { field, sourceField } of context.lookupFields) {
|
|
31422
|
-
if (!mayRecompute(sourceField.key)) continue;
|
|
31447
|
+
if (!computes(field.key) || !mayRecompute(sourceField.key)) continue;
|
|
31423
31448
|
if (linkedTableRecords.has(sourceField.table_id)) {
|
|
31424
31449
|
const linkedRecords = linkedTableRecords.get(sourceField.table_id);
|
|
31425
31450
|
const linkedFieldsArray = linkedTableFields.get(sourceField.table_id);
|
|
@@ -31467,7 +31492,7 @@ function computeFormulaFieldsWithContext(row2, fields, context, linkedTableRecor
|
|
|
31467
31492
|
if (context.circularDependencies.has(formulaField.key)) {
|
|
31468
31493
|
continue;
|
|
31469
31494
|
}
|
|
31470
|
-
if (prepared.
|
|
31495
|
+
if (prepared.parsed === void 0) {
|
|
31471
31496
|
const refs = prepared.refs.unresolved.map((ref) => `{${ref}}`).join(", ");
|
|
31472
31497
|
result[formulaField.key] = `${FORMULA_ERROR_PREFIX}${refs} ${prepared.refs.unresolved.length === 1 ? "is not a field" : "are not fields"} on this table`;
|
|
31473
31498
|
evaluationContext[formulaField.key] = null;
|
|
@@ -31485,7 +31510,7 @@ function computeFormulaFieldsWithContext(row2, fields, context, linkedTableRecor
|
|
|
31485
31510
|
}
|
|
31486
31511
|
const everyReferenceEmpty = referencedKeys.length > 0 && referencedKeys.every((fieldKey) => evaluationContext[fieldKey] === null);
|
|
31487
31512
|
const evaluated2 = evaluateFormulaWithJS(
|
|
31488
|
-
prepared.
|
|
31513
|
+
prepared.parsed,
|
|
31489
31514
|
{ data: evaluationContext },
|
|
31490
31515
|
timezone,
|
|
31491
31516
|
everyReferenceEmpty
|
|
@@ -31579,10 +31604,8 @@ function buildDependencyGraph(formulaFields, allFields) {
|
|
|
31579
31604
|
for (const field of formulaFields) {
|
|
31580
31605
|
graph.set(field.key, /* @__PURE__ */ new Set());
|
|
31581
31606
|
inDegree.set(field.key, 0);
|
|
31582
|
-
|
|
31583
|
-
|
|
31584
|
-
refs: prepareFormulaExpression(field.formula.expression, allFields)
|
|
31585
|
-
});
|
|
31607
|
+
const refs = prepareFormulaExpression(field.formula.expression, allFields);
|
|
31608
|
+
fieldMap.set(field.key, { field, refs, parsed: refs.evaluable === null ? void 0 : parseOnce(refs.evaluable) });
|
|
31586
31609
|
}
|
|
31587
31610
|
for (const field of formulaFields) {
|
|
31588
31611
|
for (const referencedKey of fieldMap.get(field.key).refs.referencedKeys) {
|
|
@@ -31662,17 +31685,18 @@ function readsFieldsOnlyThroughEmptinessPredicates(node, taken) {
|
|
|
31662
31685
|
function isFieldRead(node) {
|
|
31663
31686
|
return node !== void 0 && node.type === "Index" && node.receiver.type === "ID" && node.receiver.value === "data";
|
|
31664
31687
|
}
|
|
31665
|
-
function evaluateFormulaWithJS(
|
|
31688
|
+
function evaluateFormulaWithJS(parsed, data, timezone, everyReferenceEmpty) {
|
|
31666
31689
|
try {
|
|
31690
|
+
if (parsed.kind === "unparsable") throw parsed.error;
|
|
31691
|
+
const scope = { ...timezoneAwareFunctions(timezone), data: data.data };
|
|
31667
31692
|
if (everyReferenceEmpty) {
|
|
31668
|
-
const helpers = createTimezoneAwareFunctions(timezone);
|
|
31669
31693
|
const taken = (condition2) => {
|
|
31670
|
-
const answer = condition2.evaluate(
|
|
31694
|
+
const answer = condition2.evaluate(scope);
|
|
31671
31695
|
return typeof answer === "boolean" ? answer : void 0;
|
|
31672
31696
|
};
|
|
31673
|
-
if (!readsFieldsOnlyThroughEmptinessPredicates(
|
|
31697
|
+
if (!readsFieldsOnlyThroughEmptinessPredicates(parsed.expression, taken)) return null;
|
|
31674
31698
|
}
|
|
31675
|
-
const result =
|
|
31699
|
+
const result = parsed.expression?.evaluate(scope);
|
|
31676
31700
|
if (result === null || result === void 0) {
|
|
31677
31701
|
return null;
|
|
31678
31702
|
}
|
|
@@ -31737,18 +31761,36 @@ function planFilterTraversal(filter2, aggregatedTableId, aggregatedFields, linke
|
|
|
31737
31761
|
);
|
|
31738
31762
|
if (ids.size === 0) return null;
|
|
31739
31763
|
return {
|
|
31740
|
-
records: rows
|
|
31764
|
+
records: linkedRowsNamed(rows, [...ids]),
|
|
31741
31765
|
fields: fieldMapOf(hop.table_id, hopFields)
|
|
31742
31766
|
};
|
|
31743
31767
|
}
|
|
31744
31768
|
}
|
|
31745
31769
|
};
|
|
31746
31770
|
}
|
|
31771
|
+
var positionsOfFrozenRows = /* @__PURE__ */ new WeakMap();
|
|
31772
|
+
function linkedRowPositions(rows) {
|
|
31773
|
+
const frozen = Object.isFrozen(rows);
|
|
31774
|
+
const held2 = frozen ? positionsOfFrozenRows.get(rows) : void 0;
|
|
31775
|
+
if (held2 !== void 0) return held2;
|
|
31776
|
+
const positions = /* @__PURE__ */ new Map();
|
|
31777
|
+
for (let at2 = 0; at2 < rows.length; at2++) {
|
|
31778
|
+
const of = positions.get(rows[at2].id);
|
|
31779
|
+
if (of === void 0) positions.set(rows[at2].id, [at2]);
|
|
31780
|
+
else of.push(at2);
|
|
31781
|
+
}
|
|
31782
|
+
if (frozen) positionsOfFrozenRows.set(rows, positions);
|
|
31783
|
+
return positions;
|
|
31784
|
+
}
|
|
31785
|
+
function linkedRowsNamed(rows, ids) {
|
|
31786
|
+
const positions = linkedRowPositions(rows);
|
|
31787
|
+
const found = [...new Set(ids)].flatMap((id) => positions.get(id) ?? []);
|
|
31788
|
+
return found.sort((a, b) => a - b).map((at2) => rows[at2]);
|
|
31789
|
+
}
|
|
31747
31790
|
function calculateRollupValue(value, fieldType, allLinkedTableRecords, aggregateOption, filter2, linkedTableFields, subject, traversalContext, fileFacts) {
|
|
31748
31791
|
if (aggregateOption?.operation === "count") {
|
|
31749
31792
|
if (!value) return 0;
|
|
31750
|
-
|
|
31751
|
-
let relevantRecords2 = allLinkedTableRecords.filter((r) => recordIds2.includes(r.id));
|
|
31793
|
+
let relevantRecords2 = linkedRowsNamed(allLinkedTableRecords, extractRecordLinkIds(value));
|
|
31752
31794
|
if (filter2 && linkedTableFields) {
|
|
31753
31795
|
relevantRecords2 = filterRecords(relevantRecords2, linkedTableFields, filter2, null, traversalContext, fileFacts);
|
|
31754
31796
|
}
|
|
@@ -31757,8 +31799,7 @@ function calculateRollupValue(value, fieldType, allLinkedTableRecords, aggregate
|
|
|
31757
31799
|
if (!value || !allLinkedTableRecords.length || !aggregateOption?.field_key) {
|
|
31758
31800
|
return null;
|
|
31759
31801
|
}
|
|
31760
|
-
|
|
31761
|
-
let relevantRecords = allLinkedTableRecords.filter((record2) => recordIds.includes(record2.id));
|
|
31802
|
+
let relevantRecords = linkedRowsNamed(allLinkedTableRecords, extractRecordLinkIds(value));
|
|
31762
31803
|
if (relevantRecords.length === 0) {
|
|
31763
31804
|
return null;
|
|
31764
31805
|
}
|
|
@@ -31774,9 +31815,11 @@ function calculateLookupValue(value, linkedRecords, lookupFieldKey, linkedField,
|
|
|
31774
31815
|
if (!linkedRecords.length || !lookupFieldKey) {
|
|
31775
31816
|
return null;
|
|
31776
31817
|
}
|
|
31777
|
-
const
|
|
31778
|
-
const
|
|
31779
|
-
|
|
31818
|
+
const positions = linkedRowPositions(linkedRecords);
|
|
31819
|
+
const relevantRecords = extractRecordLinkIds(value).flatMap((id) => {
|
|
31820
|
+
const at2 = positions.get(id)?.at(-1);
|
|
31821
|
+
return at2 === void 0 ? [] : [linkedRecords[at2]];
|
|
31822
|
+
});
|
|
31780
31823
|
if (relevantRecords.length === 0) {
|
|
31781
31824
|
return null;
|
|
31782
31825
|
}
|
|
@@ -32087,7 +32130,15 @@ function groupsOf(totals, side) {
|
|
|
32087
32130
|
if (!totals.by.every(({ field_key }) => side.cells.has(field_key) && !side.unknown.has(field_key))) return null;
|
|
32088
32131
|
const [only] = totals.by;
|
|
32089
32132
|
if (totals.each && only !== void 0) return Object.keys(countsOf([{ cell: side.cells.get(only.field_key) }], "cell"));
|
|
32090
|
-
return [
|
|
32133
|
+
return [
|
|
32134
|
+
JSON.stringify(
|
|
32135
|
+
totals.by.map(({ field_key, tiers }) => {
|
|
32136
|
+
const cell = side.cells.get(field_key);
|
|
32137
|
+
if (tiers === void 0) return storedCellOf(cell);
|
|
32138
|
+
return typeof cell === "number" ? bandKey(bandOf(tiers, cell)) : null;
|
|
32139
|
+
})
|
|
32140
|
+
)
|
|
32141
|
+
];
|
|
32091
32142
|
}
|
|
32092
32143
|
function groupOfRow(totals, row2) {
|
|
32093
32144
|
return totals.each ? String(row2[VALUE_COLUMN]) : JSON.stringify(totals.by.map(({ output }) => storedCellOf(row2[output])));
|
|
@@ -32141,12 +32192,12 @@ function overlayTotals(over, totals, admits, closed, rows) {
|
|
|
32141
32192
|
function totalsAsked(shape, aggregate) {
|
|
32142
32193
|
const table = shape.rows?.table_id;
|
|
32143
32194
|
if (table === void 0 || aggregate.day !== void 0) return null;
|
|
32144
|
-
const by = (aggregate.by ?? []).map((column) => ({ output: column, field_key: fieldKeyOf(shape, column, table) }));
|
|
32195
|
+
const by = (aggregate.by ?? []).map((column) => ({ output: column, field_key: fieldKeyOf(shape, column, table), tiers: aggregate.bands?.[column] }));
|
|
32145
32196
|
const sum2 = aggregate.sum === void 0 ? void 0 : fieldKeyOf(shape, aggregate.sum, table);
|
|
32146
32197
|
if (by.some((one) => one.field_key === void 0) || aggregate.sum !== void 0 && sum2 === void 0) return null;
|
|
32147
32198
|
return {
|
|
32148
32199
|
table_id: table,
|
|
32149
|
-
by: by.flatMap(({ output, field_key }) => field_key === void 0 ? [] : [{ output, field_key }]),
|
|
32200
|
+
by: by.flatMap(({ output, field_key, tiers }) => field_key === void 0 ? [] : [{ output, field_key, ...tiers === void 0 ? {} : { tiers } }]),
|
|
32150
32201
|
aggregates: [{ output: AGGREGATE_COUNT_COLUMN, operation: "count" }, ...sum2 === void 0 ? [] : [{ output: AGGREGATE_SUM_COLUMN, operation: "sum", field_key: sum2 }]],
|
|
32151
32202
|
each: false
|
|
32152
32203
|
};
|
|
@@ -32638,75 +32689,6 @@ function useRecordActivity(entity, record_id, options = {}) {
|
|
|
32638
32689
|
};
|
|
32639
32690
|
}
|
|
32640
32691
|
|
|
32641
|
-
// src/imports.ts
|
|
32642
|
-
import { useEffect as useEffect5 } from "react";
|
|
32643
|
-
var NO_WORKFLOWS = [];
|
|
32644
|
-
var NO_RUNS = [];
|
|
32645
|
-
var NO_ROWS = [];
|
|
32646
|
-
var IMPORTS_BURST_QUIET_MS = 2e3;
|
|
32647
|
-
function useImportsLog(entity, options = {}) {
|
|
32648
|
-
const { after } = options;
|
|
32649
|
-
const { memberId, resolved } = useAppContext();
|
|
32650
|
-
const member = memberId !== null;
|
|
32651
|
-
const page = useKey(
|
|
32652
|
-
member ? JSON.stringify(["imports", entity, after ?? null]) : null,
|
|
32653
|
-
() => rpc("imports.list", after === void 0 ? {} : { after })
|
|
32654
|
-
);
|
|
32655
|
-
useAfterBurst(member ? entity : null, page.refetch);
|
|
32656
|
-
const bridged = !refusedByHost(page.error, "imports.list");
|
|
32657
|
-
const log = page.data?.kind === "log" ? page.data : void 0;
|
|
32658
|
-
return {
|
|
32659
|
-
workflows: log?.workflows ?? NO_WORKFLOWS,
|
|
32660
|
-
runs: log?.runs ?? NO_RUNS,
|
|
32661
|
-
next: log?.next,
|
|
32662
|
-
// Waiting on the context counts as loading.
|
|
32663
|
-
loading: member ? page.loading : !resolved,
|
|
32664
|
-
error: bridged ? page.error?.message ?? null : null,
|
|
32665
|
-
available: member && bridged && page.data?.kind !== "none",
|
|
32666
|
-
refetch: page.refetch
|
|
32667
|
-
};
|
|
32668
|
-
}
|
|
32669
|
-
function useImportRun(entity, run_id, options = {}) {
|
|
32670
|
-
const { after } = options;
|
|
32671
|
-
const { memberId, resolved } = useAppContext();
|
|
32672
|
-
const member = memberId !== null;
|
|
32673
|
-
const page = useKey(
|
|
32674
|
-
member ? JSON.stringify(["import_run", entity, run_id, after ?? null]) : null,
|
|
32675
|
-
() => rpc("imports.run", { run_id, ...after === void 0 ? {} : { after } })
|
|
32676
|
-
);
|
|
32677
|
-
const read2 = page.data?.kind === "run" ? page.data : void 0;
|
|
32678
|
-
useAfterBurst(member && read2?.run.end === "running" ? entity : null, page.refetch);
|
|
32679
|
-
const bridged = !refusedByHost(page.error, "imports.run");
|
|
32680
|
-
return {
|
|
32681
|
-
run: read2?.run,
|
|
32682
|
-
rows: read2?.rows ?? NO_ROWS,
|
|
32683
|
-
next: read2?.next,
|
|
32684
|
-
loading: member ? page.loading : !resolved,
|
|
32685
|
-
error: bridged ? page.error?.message ?? null : null,
|
|
32686
|
-
available: member && bridged && page.data?.kind !== "none",
|
|
32687
|
-
refetch: page.refetch
|
|
32688
|
-
};
|
|
32689
|
-
}
|
|
32690
|
-
function useAfterBurst(entity, ask2) {
|
|
32691
|
-
useEffect5(() => {
|
|
32692
|
-
if (entity === null) return;
|
|
32693
|
-
const alias = listQueryAlias(entity);
|
|
32694
|
-
let quiet;
|
|
32695
|
-
const unsubscribe = subscribeQueriesChanged((aliases) => {
|
|
32696
|
-
if (!aliases.includes(alias)) return;
|
|
32697
|
-
clearTimeout(quiet);
|
|
32698
|
-
quiet = setTimeout(ask2, IMPORTS_BURST_QUIET_MS);
|
|
32699
|
-
});
|
|
32700
|
-
return () => {
|
|
32701
|
-
clearTimeout(quiet);
|
|
32702
|
-
unsubscribe();
|
|
32703
|
-
};
|
|
32704
|
-
}, [entity, ask2]);
|
|
32705
|
-
}
|
|
32706
|
-
function refusedByHost(error51, op) {
|
|
32707
|
-
return error51?.message === `Unknown RPC op: ${op}`;
|
|
32708
|
-
}
|
|
32709
|
-
|
|
32710
32692
|
// src/recording.ts
|
|
32711
32693
|
import { useCallback as useCallback7, useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
32712
32694
|
var NO_MOCKS = {};
|
|
@@ -32905,7 +32887,7 @@ function readSelect(value) {
|
|
|
32905
32887
|
}
|
|
32906
32888
|
|
|
32907
32889
|
// src/use_recents.ts
|
|
32908
|
-
import { useCallback as useCallback8, useEffect as
|
|
32890
|
+
import { useCallback as useCallback8, useEffect as useEffect5, useRef as useRef4, useState as useState4 } from "react";
|
|
32909
32891
|
var PREFIX = "lotics.recents.";
|
|
32910
32892
|
function read(storageKey) {
|
|
32911
32893
|
try {
|
|
@@ -32930,7 +32912,7 @@ function useRecents(key, options = {}) {
|
|
|
32930
32912
|
const maxRef = useRef4(options.max ?? 5);
|
|
32931
32913
|
maxRef.current = options.max ?? 5;
|
|
32932
32914
|
const [recents, setRecents] = useState4(() => read(storageKey).slice(0, maxRef.current));
|
|
32933
|
-
|
|
32915
|
+
useEffect5(() => {
|
|
32934
32916
|
setRecents(read(storageKey).slice(0, maxRef.current));
|
|
32935
32917
|
}, [storageKey]);
|
|
32936
32918
|
const remember = useCallback8(
|
|
@@ -32963,13 +32945,13 @@ function useRecents(key, options = {}) {
|
|
|
32963
32945
|
}
|
|
32964
32946
|
|
|
32965
32947
|
// src/use_url_state.ts
|
|
32966
|
-
import { useCallback as useCallback9, useEffect as
|
|
32948
|
+
import { useCallback as useCallback9, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef5, useState as useState5 } from "react";
|
|
32967
32949
|
function useUrlState(defs) {
|
|
32968
32950
|
const defsRef = useRef5(defs);
|
|
32969
32951
|
defsRef.current = defs;
|
|
32970
32952
|
const [params, setParams] = useState5(peekUrlParams);
|
|
32971
32953
|
const editedRef = useRef5(false);
|
|
32972
|
-
|
|
32954
|
+
useEffect6(() => {
|
|
32973
32955
|
let active = true;
|
|
32974
32956
|
const apply = (next) => {
|
|
32975
32957
|
if (active) setParams((prev) => paramsEqual(prev, next) ? prev : next);
|
|
@@ -33061,8 +33043,6 @@ export {
|
|
|
33061
33043
|
useFieldOptions,
|
|
33062
33044
|
useFileUpload,
|
|
33063
33045
|
useFolderPick,
|
|
33064
|
-
useImportRun,
|
|
33065
|
-
useImportsLog,
|
|
33066
33046
|
useMembers,
|
|
33067
33047
|
useNewRecord,
|
|
33068
33048
|
useQueries,
|
package/dist/router.js
CHANGED
package/dist/rpc.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
|
|
|
9
9
|
* app → host: { id, op, payload }
|
|
10
10
|
* host → app: { id, type: "result", data } | { id, type: "error", message }
|
|
11
11
|
*/
|
|
12
|
-
export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "activity.list" | "
|
|
12
|
+
export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "activity.list" | "recording.start" | "recording.stop";
|
|
13
13
|
export interface AgentRunPayload {
|
|
14
14
|
alias: string;
|
|
15
15
|
session_id: string;
|
package/dist/shared_types.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* `scripts/build_sdk.mjs`, because that package is private and ships in no
|
|
4
4
|
* tarball. Written at every build, never edited.
|
|
5
5
|
*/
|
|
6
|
-
export type ActivityEntry = { changes: { field_id: string; label: string; before: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; after: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; removed?: true | undefined; }[]; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "change"; created?: true | undefined; } | { comment: { id: string; body: string; mentions:
|
|
7
|
-
export type AppAbsence = { member:
|
|
6
|
+
export type ActivityEntry = { changes: { field_id: string; label: string; before: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; after: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; removed?: true | undefined; }[]; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "change"; created?: true | undefined; } | { comment: { id: string; body: string; mentions: { id: string; name: string; }[]; files?: { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | undefined; }; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "comment"; };
|
|
7
|
+
export type AppAbsence = { member: { id: string; name: string; }; stand_in: { id: string; name: string; }; starts_on: string; ends_on: string; group_ids: string[]; };
|
|
8
8
|
/** The file an app export made, as a link to download it: no workspace file, so no id, and kept a day. */
|
|
9
9
|
export type ExportedFile = {
|
|
10
10
|
filename: string;
|
|
@@ -16,12 +16,14 @@ export type ExportedFile = {
|
|
|
16
16
|
/** When the server made it, ISO 8601. */
|
|
17
17
|
created_at: string;
|
|
18
18
|
};
|
|
19
|
-
export type ImportRow = { record_id: string; kind: "removed" | "added" | "changed"; at: string; changes: { field_id: string; label: string; before: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; after: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; removed?: true | undefined; }[]; };
|
|
20
|
-
export type ImportRun = { id: string; workflow: ImportWorkflow; at: string; end: "running" | "done" | "failed" | "partial"; added: number; changed: number; removed: number; until?: string | undefined; stopped?: { step?: string | undefined; reason?: string | undefined; } | undefined; };
|
|
21
|
-
export type ImportWorkflow = { id: string; name: string; };
|
|
22
19
|
/** What an aggregate asks: output columns to group by, an output date grouped by its day, an output number summed. */
|
|
23
20
|
export type QueryAggregate = {
|
|
24
21
|
by?: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* The `by` columns grouped by the band of their number in place of its value, each with its ascending
|
|
24
|
+
* breakpoints: the group answers the band's key (`bandKey`) under the column, null for the rows holding no number.
|
|
25
|
+
*/
|
|
26
|
+
bands?: { readonly [x: string]: readonly number[]; };
|
|
25
27
|
day?: string;
|
|
26
28
|
sum?: string;
|
|
27
29
|
};
|
package/docs/data_fetching.md
CHANGED
|
@@ -93,7 +93,7 @@ all its calls, and each call states its own `filter`, `sort`, `total` and `rows`
|
|
|
93
93
|
|---|---|---|---|
|
|
94
94
|
| `enabled` | `boolean` | `true` | `false` = no request is sent, `rows` is `[]`, `loading` is `false`. Flip to `true` to fetch. The gate for search-as-you-type and on-demand detail. Disabling also **hides** previously loaded rows (the cached answer survives; it re-renders instantly when re-enabled). |
|
|
95
95
|
| `revalidateOnFocus` | `boolean` | `true` | `false` = no re-read on window focus / tab return / network reconnect (`refetch()` still works). Keep the default for dashboards; turn off for transient queries (a search bound to an ephemeral term) where a refocus re-run is wasted work and a visible reload. |
|
|
96
|
-
| `revalidateOnChange` | `boolean` | `true` | `false` = no re-read on a realtime push or this app's own write over the query's tables: the rows stay as first read until `refetch()`, this app's own writes still drawn over them — a moment's rows
|
|
96
|
+
| `revalidateOnChange` | `boolean` | `true` | `false` = no re-read on a realtime push or this app's own write over the query's tables: the rows stay as first read until `refetch()`, this app's own writes still drawn over them — a moment's rows, read beside a live screen, that never join the render a push draws. A read of the same call left on the default is its own read, live. |
|
|
97
97
|
| `sort` | `QuerySortKey[]` | — | Runtime sort, applied server-side **after** the named query, over its output columns, each key breaking the ties of the ones before: `{ field_key, order: "asc" \| "desc", blank_position? }` a column's order, or `{ first: [filter, …] }` the rows each filter keeps, in turn, before the rest ([queries](./queries.md)). An empty/omitted array leaves the query's own order intact. Part of the read's key — changing it re-queries. |
|
|
98
98
|
| `filter` | `QueryFilter` | — | Runtime filter, applied server-side after the named query, over its output columns. A single condition or a recursive `{ node_type: "group", logic: "and" \| "or", children }`. Part of the read's key. |
|
|
99
99
|
| `limit` | `number` | — | The first `limit` rows only — a **cap**, not pages; omit to read up to the server's row cap. |
|
|
@@ -417,45 +417,6 @@ const older = useRecordActivity("job", recordId, { after: next });
|
|
|
417
417
|
- **Freshness:** read again when a push or this app's own write names the entity's reads, when a
|
|
418
418
|
comment is written or deleted through `useComments`, on focus, and on `refetch()`.
|
|
419
419
|
|
|
420
|
-
## A register's imports log: `useImportsLog`, `useImportRun`
|
|
421
|
-
|
|
422
|
-
The workflows marked as importing into the register's table (an automation's `imports_into`) and
|
|
423
|
-
their runs, each with how it ended and how many rows it added, changed and removed; and one run's
|
|
424
|
-
rows, each with its fields before and after (`dist/imports.d.ts`). A JSON app whose model states
|
|
425
|
-
`register.imports: true` keeps it. `entity` is the register's entity alias.
|
|
426
|
-
|
|
427
|
-
```tsx
|
|
428
|
-
const { workflows, runs, next, loading, error, available, refetch } = useImportsLog("order");
|
|
429
|
-
const { run, rows } = useImportRun("order", runs[0]?.id ?? "");
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
- **Authority: the app's own reads**, as record activity's: only the fields the app reads appear, and
|
|
433
|
-
a row counts — and is listed — only where the register's own read reaches it for the viewer; a
|
|
434
|
-
removed row, where it reached it before it was removed. A row the viewer does not see is in no
|
|
435
|
-
count and does not make a failed run `partial`.
|
|
436
|
-
- **Members of the app's organization only.** Standalone, in a host that bridges no imports log, and
|
|
437
|
-
where the register keeps none (the server answers `{ kind: "none" }`), `available` is `false`,
|
|
438
|
-
`error` is `null` and nothing is shown. Any other refusal is an `error`.
|
|
439
|
-
- **The workflows** (`ImportWorkflow`): `{ id, name }`, every one importing into the table whether
|
|
440
|
-
or not it has run, by name.
|
|
441
|
-
- **A run** (`ImportRun`): `{ id, workflow: { id, name }, at, until?, end, added, changed, removed,
|
|
442
|
-
stopped? }`. `end` is `running`, `done`, `failed` (it wrote none of the rows the viewer sees) or
|
|
443
|
-
`partial` (it failed after writing some). `until` is absent while it runs. A `failed` or `partial`
|
|
444
|
-
run carries `stopped: { step?, reason? }` (`ImportRunStop`): the description of the step it stopped
|
|
445
|
-
at, for every viewer; and the error it recorded there, only for a viewer who manages the automation
|
|
446
|
-
(it can quote data from tables the viewer cannot open) — each absent where the run recorded none.
|
|
447
|
-
- **A run's row** (`ImportRow`): `{ record_id, kind, at, changes }`, one per row however many times
|
|
448
|
-
the run wrote it — `added`, `removed`, or `changed` (a field the viewer sees left other than the
|
|
449
|
-
run found it). `changes` are record activity's (`ActivityChange`): an added row's fields from
|
|
450
|
-
`null`, a removed row's to `null`. Rows come in the order the run first wrote them.
|
|
451
|
-
- **Pages**: up to twenty runs — fewer where they wrote thousands of rows between them, always at
|
|
452
|
-
least one — or fifty rows; `next` continues — `undefined` at the end. A page of rows may hold fewer
|
|
453
|
-
and still say `next`, where the rows it looked through were ones the viewer does not see.
|
|
454
|
-
- **Freshness:** the log is read again once pushes naming the entity's rows have stopped for two
|
|
455
|
-
seconds — an import's writes landing — never with each push, so the register's own rows never wait
|
|
456
|
-
on it; a run's rows only while it runs. Both are read again on focus and on `refetch()`. A run that
|
|
457
|
-
wrote nothing pushes nothing.
|
|
458
|
-
|
|
459
420
|
## Where each hook works
|
|
460
421
|
|
|
461
422
|
| Surface | Embedded (signed-in member) | Standalone / public (anonymous) |
|
|
@@ -468,4 +429,3 @@ const { run, rows } = useImportRun("order", runs[0]?.id ?? "");
|
|
|
468
429
|
| `useWorkspaceTimezone` / `useWorkspaceCurrency` | ✓ | ✓ |
|
|
469
430
|
| `useComments` / `useCommentCounts` | ✓ when capability declared | ✗ `available: false` |
|
|
470
431
|
| `useRecordActivity` | ✓ the app's organization's members | ✗ `available: false` |
|
|
471
|
-
| `useImportsLog` / `useImportRun` | ✓ the app's organization's members | ✗ `available: false` |
|
package/docs/mutations.md
CHANGED
|
@@ -279,7 +279,7 @@ When the alias declares `inputs`, the server validates the payload before the wo
|
|
|
279
279
|
is **drift-proof**: the union is resolved from the field's *current* options at type-gen, and
|
|
280
280
|
the submitted value is validated against the field's *current* options at run time — an option
|
|
281
281
|
added after authoring is accepted, a removed one rejected
|
|
282
|
-
(`select input "<path>" value "<v>" is not one of
|
|
282
|
+
(`select input "<path>" value "<v>" is not one of its current options (<label>=<value>, …)`) with no
|
|
283
283
|
redeploy. A `field` that doesn't exist or names a non-select field is rejected at bind time
|
|
284
284
|
(`set_app_workflow`) with
|
|
285
285
|
`select input references field "<key>", which does not exist in this workspace` /
|
|
@@ -527,10 +527,10 @@ every read before the request answers:
|
|
|
527
527
|
leaves it; a created row it admits joins it, standing by the read's order (the call's `sort`,
|
|
528
528
|
then the query's own) where its sort cells are numbers or dates, else first. A row the read does
|
|
529
529
|
not hold that a write may move into it waits for the server, and `pending` says so.
|
|
530
|
-
- **Totals.** A `total`, a count per value (`total: { by }`) or per named filter (`total: { where }`), a group asked with `aggregate`
|
|
531
|
-
declared by the query, and a parent's count or sum rollup move by exactly what the written row adds
|
|
530
|
+
- **Totals.** A `total`, a count per value (`total: { by }`) or per named filter (`total: { where }`), a group asked with `aggregate` (by a
|
|
531
|
+
number's `bands` too) or declared by the query, and a parent's count or sum rollup move by exactly what the written row adds
|
|
532
532
|
or takes away; a group the last row leaves is gone, and a count per value gains the value a row
|
|
533
|
-
first holds. A mean, an extreme, a distinct count, a group by day, an aggregate with its own
|
|
533
|
+
first holds. A mean, an extreme, a distinct count, a group by day or by a declared band key, an aggregate with its own
|
|
534
534
|
`filter`, a group none of the read's rows counted in yet, a read whose rows a search or a cap
|
|
535
535
|
decides, or a row the app never read is left
|
|
536
536
|
as the server said it until a read asked after the write answers.
|
package/docs/queries.md
CHANGED
|
@@ -775,6 +775,25 @@ Sort by the bucket column for a time series; filter it with date operators for a
|
|
|
775
775
|
window. **Always bucket server-side** — shipping raw rows to bucket in JS burns the row cap and
|
|
776
776
|
the timeout for nothing.
|
|
777
777
|
|
|
778
|
+
### Band group keys
|
|
779
|
+
|
|
780
|
+
`group.by` also accepts a number read by the bands its breakpoints cut:
|
|
781
|
+
|
|
782
|
+
```jsonc
|
|
783
|
+
{ "band": { "source": "days_late", "tiers": [1, 31, 61], "output": "age" } }
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
- `source` must be a `number` **column** of the input (checked at bind); `tiers` are its breakpoints,
|
|
787
|
+
ascending, at least one.
|
|
788
|
+
- The output is a new **`text`** column holding the band as `from..to` — the two breakpoints a row's
|
|
789
|
+
number stands between, the lower held and the upper not, an open end empty: `..1`, `1..31`,
|
|
790
|
+
`31..61`, `61..`. A number on a breakpoint is in the band above it. A row holding no number
|
|
791
|
+
yields a NULL band (its rows group together).
|
|
792
|
+
- A band no row falls in is no group: the answer holds only the bands with rows.
|
|
793
|
+
|
|
794
|
+
One group per band, however many distinct numbers the rows hold — grouping by the number itself
|
|
795
|
+
answers a group per value, which the row cap cuts.
|
|
796
|
+
|
|
778
797
|
**Window `aggregates` — the OVER-legal aggregate subset.** Only operations that compile to a single
|
|
779
798
|
legal SQL window call are accepted: **`count`, `sum`, `avg`, `min`, `max`, `earliest`, `latest`,
|
|
780
799
|
`filled`, `checked`, `unchecked`.** The rest cannot take an OVER clause (`median` is an
|
|
@@ -870,10 +889,12 @@ template as derived nodes, in this order: `filter` (narrow) → `sort` (order)
|
|
|
870
889
|
at most 32. It is a
|
|
871
890
|
**second full execution** of the template — sent only where a read asks for its `total`, and
|
|
872
891
|
one count serves every read of the same set ([data_fetching](./data_fetching.md)).
|
|
873
|
-
- **`aggregate: { by?, day?, sum? }`** — the groups of the *filtered* set in place of its rows: one
|
|
874
|
-
row per value of up to
|
|
892
|
+
- **`aggregate: { by?, bands?, day?, sum? }`** — the groups of the *filtered* set in place of its rows: one
|
|
893
|
+
row per value of up to three `by` output columns (a cell holding several values groups by all of
|
|
875
894
|
them) and per day of a `day` output date, each holding its `by` columns, `__day`, `__count`, and
|
|
876
|
-
`__sum` of a `sum` output number (`null` where no row holds one).
|
|
895
|
+
`__sum` of a `sum` output number (`null` where no row holds one). `bands: { <column>: [breakpoints] }`
|
|
896
|
+
groups a number column of `by` by its band in place of its value — the column then holds `from..to`
|
|
897
|
+
as a band group key does (above), `null` for the rows holding no number. Every column it names is an
|
|
877
898
|
output column, like `filter`'s, so what leaves is counts and sums of what a page of the same
|
|
878
899
|
query returns. Sent with `params` and `filter` alone; `truncated` past the row cap. One read
|
|
879
900
|
over a list's rows serves the figures above it — its filter is the list's own.
|
package/docs/security.md
CHANGED
|
@@ -11,7 +11,6 @@ Every data operation an app performs — queries, workflows, agent runs — exec
|
|
|
11
11
|
| Agent runs (`useAgentRun`) | App owner | Yes — requires an authenticated member; runs are private to that member |
|
|
12
12
|
| Comments (`useComments`) | App authority for **access**; the **author** is always the real member | Always — members-only, anonymous callers are rejected |
|
|
13
13
|
| Record activity (`useRecordActivity`) | The app's own read of the record — the entity's read of one row, and only the fields it draws | Always — the app's organization's members only |
|
|
14
|
-
| Imports log (`useImportsLog`, `useImportRun`) | The register's own read — only the rows it reaches for the viewer, and only the fields the app reads | Always — the app's organization's members only |
|
|
15
14
|
|
|
16
15
|
Consequences of owner authority:
|
|
17
16
|
|
|
@@ -104,7 +103,6 @@ A publicly-shared app (its own origin, or its public link) is reachable by **any
|
|
|
104
103
|
| Agent runs (`useAgentRun`) | **No** — rejected: agent runs require an authenticated member, and each member's run history is private to them (a guessed session id cannot read another member's thread) |
|
|
105
104
|
| Comments (`useComments`) | **No** — members-only |
|
|
106
105
|
| Record activity (`useRecordActivity`) | **No** — the app's organization's members only |
|
|
107
|
-
| Imports log (`useImportsLog`, `useImportRun`) | **No** — the app's organization's members only |
|
|
108
106
|
| Member roster (`useMembers`) | **No** — same-org members only (below) |
|
|
109
107
|
|
|
110
108
|
**Warning — `is_current_member` on a public app resolves to the app owner.** An anonymous request has no viewer to bind, so both `is_current_member` and `current_member in_any_group` fall back to the owner's identity and the owner's groups. A "my items" query shows every anonymous visitor the *owner's* items, and a manager-only read template gated on a group the owner belongs to **passes for every anonymous visitor**. Never rely on viewer-bound predicates in a public app.
|
package/package.json
CHANGED
package/dist/imports.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { ImportRow, ImportRun, ImportWorkflow } from "./shared_types.js";
|
|
2
|
-
export interface ImportsLogState {
|
|
3
|
-
/** The workflows importing into the register's table, whether or not they have run. */
|
|
4
|
-
workflows: ImportWorkflow[];
|
|
5
|
-
/** Newest first. */
|
|
6
|
-
runs: ImportRun[];
|
|
7
|
-
/** Where the log goes on — pass it as `after` to read the page past this one; `undefined` at its end. */
|
|
8
|
-
next: string | undefined;
|
|
9
|
-
loading: boolean;
|
|
10
|
-
error: string | null;
|
|
11
|
-
/** A signed-in member, in a host that bridges the log, of a register keeping one; otherwise `runs` stays empty. */
|
|
12
|
-
available: boolean;
|
|
13
|
-
refetch: () => void;
|
|
14
|
-
}
|
|
15
|
-
export interface ImportRunState {
|
|
16
|
-
/** The run, as the log lists it; `undefined` until read. */
|
|
17
|
-
run: ImportRun | undefined;
|
|
18
|
-
/** In the order the run first wrote them. */
|
|
19
|
-
rows: ImportRow[];
|
|
20
|
-
/** Where its rows go on — pass it as `after`; `undefined` at their end. */
|
|
21
|
-
next: string | undefined;
|
|
22
|
-
loading: boolean;
|
|
23
|
-
error: string | null;
|
|
24
|
-
available: boolean;
|
|
25
|
-
refetch: () => void;
|
|
26
|
-
}
|
|
27
|
-
export interface ImportsPageOptions {
|
|
28
|
-
/** A page's `next`: read the page past it. Absent, the first page. */
|
|
29
|
-
after?: string;
|
|
30
|
-
}
|
|
31
|
-
/** No push over the register's rows for this long ends a burst: longer than the second the server waits at most
|
|
32
|
-
* between pushes while a run writes. */
|
|
33
|
-
export declare const IMPORTS_BURST_QUIET_MS = 2000;
|
|
34
|
-
/**
|
|
35
|
-
* One page of the register's imports log. `entity` is the alias of the register's entity: the page is asked again once
|
|
36
|
-
* pushes naming its rows have gone quiet.
|
|
37
|
-
*
|
|
38
|
-
* ```tsx
|
|
39
|
-
* const { runs, next } = useImportsLog("order");
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export declare function useImportsLog(entity: string, options?: ImportsPageOptions): ImportsLogState;
|
|
43
|
-
/**
|
|
44
|
-
* One page of one run's rows — `run_id` a run's `id` from `useImportsLog` — with the run beside them. Asked again after
|
|
45
|
-
* a burst of pushes over the register's rows only while the run runs: an ended run's rows are what they are.
|
|
46
|
-
*
|
|
47
|
-
* ```tsx
|
|
48
|
-
* const { run, rows, next } = useImportRun("order", runId);
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
51
|
-
export declare function useImportRun(entity: string, run_id: string, options?: ImportsPageOptions): ImportRunState;
|