@standardbeagle/edit-db 0.4.484 → 0.4.581

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.
Files changed (111) hide show
  1. package/dist/common/fetcher.d.ts +7 -2
  2. package/dist/common/saved-objects.d.ts +40 -0
  3. package/dist/common/schema-queries.d.ts +1 -0
  4. package/dist/components/bulk-edit-dialog.d.ts +18 -0
  5. package/dist/components/content-panel.d.ts +5 -2
  6. package/dist/components/content-viewer.d.ts +8 -1
  7. package/dist/components/content-viewer.test.d.ts +1 -0
  8. package/dist/components/data-table.d.ts +49 -2
  9. package/dist/components/detail-panel.d.ts +2 -2
  10. package/dist/components/export-button.d.ts +19 -0
  11. package/dist/components/export-button.test.d.ts +1 -0
  12. package/dist/components/filters/fk-filter.d.ts +2 -0
  13. package/dist/components/filters/fk-filter.test.d.ts +1 -0
  14. package/dist/components/filters/number-filter.d.ts +8 -0
  15. package/dist/components/filters/number-filter.test.d.ts +1 -0
  16. package/dist/components/fk-cell-popover.d.ts +12 -5
  17. package/dist/components/fk-cell-popover.test.d.ts +1 -0
  18. package/dist/components/form-runner-host.d.ts +14 -0
  19. package/dist/components/form-runner.d.ts +43 -0
  20. package/dist/components/form-runner.test.d.ts +0 -0
  21. package/dist/components/form-subform.d.ts +11 -0
  22. package/dist/components/form-subform.test.d.ts +0 -0
  23. package/dist/components/grouped-grid-summary.d.ts +17 -0
  24. package/dist/components/grouped-grid-summary.test.d.ts +0 -0
  25. package/dist/components/m2m-panel.d.ts +4 -5
  26. package/dist/components/ui/table.test.d.ts +1 -0
  27. package/dist/data-edit.d.ts +25 -4
  28. package/dist/data-panel.d.ts +0 -11
  29. package/dist/edit-db.css +1 -1
  30. package/dist/editor.d.ts +22 -1
  31. package/dist/editor.es.js +7368 -4589
  32. package/dist/editor.stories.d.ts +0 -1
  33. package/dist/editor.umd.cjs +48 -27
  34. package/dist/hooks/useColumnNav.d.ts +3 -3
  35. package/dist/hooks/useColumnSizingPersistence.d.ts +19 -0
  36. package/dist/hooks/useColumnVisibilityPersistence.d.ts +34 -0
  37. package/dist/hooks/useColumnVisibilityPersistence.test.d.ts +1 -0
  38. package/dist/hooks/useCompositeTableRef.d.ts +20 -2
  39. package/dist/hooks/useCompositeTableRef.test.d.ts +1 -0
  40. package/dist/hooks/useDataTable.d.ts +59 -6
  41. package/dist/hooks/useDebouncedCommit.d.ts +19 -0
  42. package/dist/hooks/useDebouncedCommit.test.d.ts +1 -0
  43. package/dist/hooks/useDeltaMutation.d.ts +20 -0
  44. package/dist/hooks/useDeltaMutation.test.d.ts +0 -0
  45. package/dist/hooks/useEditorConfig.d.ts +27 -0
  46. package/dist/hooks/useFitMode.d.ts +23 -0
  47. package/dist/hooks/useFitMode.test.d.ts +1 -0
  48. package/dist/hooks/usePanelSize.d.ts +28 -0
  49. package/dist/hooks/usePanelSize.test.d.ts +1 -0
  50. package/dist/hooks/usePath.d.ts +2 -5
  51. package/dist/hooks/useRowHoverActions.d.ts +26 -0
  52. package/dist/hooks/useSchema.d.ts +8 -2
  53. package/dist/hooks/useSchemaFilter.test.d.ts +1 -0
  54. package/dist/hooks/useTableExport.d.ts +13 -0
  55. package/dist/hooks/useTableMutation.d.ts +1 -4
  56. package/dist/hooks/useTableMutation.test.d.ts +0 -0
  57. package/dist/hooks/useTableRef.d.ts +23 -2
  58. package/dist/hooks/useTableRef.test.d.ts +1 -0
  59. package/dist/hooks/useTableStats.d.ts +7 -24
  60. package/dist/hooks/useToast.d.ts +15 -0
  61. package/dist/index.d.ts +13 -1
  62. package/dist/lib/column-order.d.ts +17 -0
  63. package/dist/lib/column-order.test.d.ts +1 -0
  64. package/dist/lib/content-detect.d.ts +39 -0
  65. package/dist/lib/content-detect.sniff.test.d.ts +1 -0
  66. package/dist/lib/date-input.d.ts +21 -0
  67. package/dist/lib/date-input.test.d.ts +1 -0
  68. package/dist/lib/drill-stack.d.ts +19 -2
  69. package/dist/lib/export.d.ts +122 -0
  70. package/dist/lib/export.test.d.ts +1 -0
  71. package/dist/lib/field-validation.d.ts +16 -0
  72. package/dist/lib/fk.d.ts +23 -2
  73. package/dist/lib/form-definition.d.ts +57 -0
  74. package/dist/lib/form-definition.test.d.ts +1 -0
  75. package/dist/lib/form-runner-nav.d.ts +19 -0
  76. package/dist/lib/form-runner-nav.test.d.ts +1 -0
  77. package/dist/lib/form-subform.d.ts +12 -0
  78. package/dist/lib/form-subform.test.d.ts +1 -0
  79. package/dist/lib/form-widget.d.ts +36 -0
  80. package/dist/lib/form-widget.test.d.ts +1 -0
  81. package/dist/lib/format-value.d.ts +26 -0
  82. package/dist/lib/grid-grouping.d.ts +63 -0
  83. package/dist/lib/grid-grouping.test.d.ts +1 -0
  84. package/dist/lib/invalidate.d.ts +7 -0
  85. package/dist/lib/invalidate.test.d.ts +1 -0
  86. package/dist/lib/label-match.d.ts +13 -0
  87. package/dist/lib/label-match.test.d.ts +1 -0
  88. package/dist/lib/m2m.d.ts +45 -6
  89. package/dist/lib/mutation-payload.d.ts +37 -0
  90. package/dist/lib/mutation-payload.test.d.ts +1 -0
  91. package/dist/lib/query-builder.d.ts +137 -8
  92. package/dist/lib/row-id.d.ts +20 -0
  93. package/dist/lib/rows-by-pk-query.test.d.ts +1 -0
  94. package/dist/lib/scalar-types.d.ts +30 -0
  95. package/dist/lib/scalar-types.test.d.ts +1 -0
  96. package/dist/lib/table-action.d.ts +24 -0
  97. package/dist/lib/table-ref.d.ts +45 -0
  98. package/dist/lib/table-ref.test.d.ts +1 -0
  99. package/dist/{tableList.d.ts → table-list.d.ts} +1 -1
  100. package/dist/table-list.test.d.ts +0 -0
  101. package/dist/table-view.chart.test.d.ts +1 -0
  102. package/dist/table-view.d.ts +22 -0
  103. package/dist/table-view.test.d.ts +0 -0
  104. package/dist/types/schema.d.ts +54 -10
  105. package/package.json +2 -4
  106. package/dist/common/schema.d.ts +0 -2
  107. package/dist/data-data-table.d.ts +0 -17
  108. package/dist/error-panel.d.ts +0 -1
  109. /package/dist/common/{schema.test.d.ts → saved-objects.test.d.ts} +0 -0
  110. /package/dist/{components/table-dimensions.test.d.ts → common/schema-queries.test.d.ts} +0 -0
  111. /package/dist/{tableList.test.d.ts → components/bulk-edit-dialog.test.d.ts} +0 -0
@@ -4,8 +4,25 @@ export type ContentKind = 'json' | 'xml' | 'php-serialized' | 'binary' | 'longte
4
4
  export declare function isBinaryDbType(dbType: string): boolean;
5
5
  /** Check if a dbType is a native JSON type (json, jsonb). */
6
6
  export declare function isJsonDbType(dbType: string): boolean;
7
+ /** Column-level JSON check: native JSON dbType or the JSON GraphQL scalar. */
8
+ export declare function isJsonColumn(col: {
9
+ dbType: string;
10
+ paramType?: string;
11
+ }): boolean;
7
12
  /** Check if a dbType is a long text type (text, ntext, varchar(max), xml) */
8
13
  export declare function isLongTextDbType(dbType: string): boolean;
14
+ /**
15
+ * Column-level "large value" check for fetch-on-demand decisions (grid SELECT
16
+ * exclusion, sortability, content-viewer cells). Prefers the server's
17
+ * dialect-decided `dbColumnSchema.isLargeValue` flag; the dbType-name fallback
18
+ * exists only for servers that predate the flag. The name heuristic is WRONG on
19
+ * PostgreSQL and SQLite — `text` is their ordinary string type, not a LOB — which
20
+ * is exactly why the server flag is authoritative when present.
21
+ */
22
+ export declare function isLargeValueColumn(col: {
23
+ dbType: string;
24
+ isLargeValue?: boolean;
25
+ }): boolean;
9
26
  /** Detect content kind from a string value and optional dbType hint */
10
27
  export declare function detectContentKind(value: string, dbType?: string): ContentKind;
11
28
  /** Max characters to show in a table cell before truncating */
@@ -14,3 +31,25 @@ export declare const CELL_TRUNCATE = 80;
14
31
  export declare function truncateForCell(value: string): string;
15
32
  /** Format binary data (base64 string) for display */
16
33
  export declare function formatBinaryPreview(value: string): string;
34
+ /** What a binary (base64) value actually IS, sniffed from its magic bytes. */
35
+ export type SniffedBinary = {
36
+ kind: 'image';
37
+ mime: string;
38
+ extension: string;
39
+ } | {
40
+ kind: 'pdf';
41
+ mime: 'application/pdf';
42
+ extension: '.pdf';
43
+ } | {
44
+ kind: 'unknown';
45
+ mime: 'application/octet-stream';
46
+ extension: '.bin';
47
+ };
48
+ /**
49
+ * Sniffs an image/document type from the FIRST BYTES of a base64 value — never
50
+ * from a column name or a user-supplied mime string, so a mislabeled blob can
51
+ * only ever downgrade to the generic binary rendering, not upgrade to <img>.
52
+ */
53
+ export declare function sniffBinaryContent(base64: string): SniffedBinary;
54
+ /** A data: URL for an inline binary preview (only call for sniffed images). */
55
+ export declare function binaryDataUrl(base64: string, mime: string): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { Column } from '../types/schema';
2
+ /** True for any date-ish column (bare date or datetime). */
3
+ export declare function isDateColumn(column: Column): boolean;
4
+ /** True when a date-ish column carries a time-of-day component (datetime, not a bare date). */
5
+ export declare function isDateTimeColumn(column: Column): boolean;
6
+ /**
7
+ * Normalizes a stored date/datetime string into the value an `<input type="date">`
8
+ * or `<input type="datetime-local">` expects. For datetime columns the time-of-day
9
+ * is preserved (fractional seconds and timezone offset are trimmed) so re-saving an
10
+ * unrelated field does not overwrite the time with midnight. Returns '' for the
11
+ * SQL "zero" date and unparseable input.
12
+ */
13
+ export declare function toDateInputValue(raw: string | undefined, withTime: boolean): string;
14
+ /**
15
+ * Restore the original raw value for every date/datetime field the user did not
16
+ * edit. The form holds `toDateInputValue` projections; a field whose current form
17
+ * value still equals the projection of the original raw value is untouched, so
18
+ * the raw value (offset and fractional seconds intact) is sent instead of the
19
+ * lossy projection. Fields the user actually changed keep the new form value.
20
+ */
21
+ export declare function preserveUntouchedDateValues(formValues: Record<string, unknown>, originalRow: Record<string, unknown>, columns: Column[]): Record<string, unknown>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,25 @@
1
- import { ColumnPanel } from '../data-panel';
1
+ /**
2
+ * @module lib/drill-stack
3
+ *
4
+ * Pure-logic helpers for the stackable parent/child drill-down used by the
5
+ * edit-db data panel. Each entry in the stack describes a side column: a
6
+ * target table plus an optional filter (FK column + parent row id) that
7
+ * scopes the child rows to those related to a row in the previous level.
8
+ *
9
+ * Kept dependency-free so it can be unit-tested without React or the DOM.
10
+ */
2
11
  /**
3
12
  * A single frame in the drill navigation chain, visible as one side column.
13
+ * `filterTable` + `filterId` + `filterColumn` describe the FK scoping applied
14
+ * to `tableName`'s rows — they're optional so the same shape can also
15
+ * represent a simple "open table X" panel.
4
16
  */
5
- export type DrillFrame = ColumnPanel;
17
+ export interface DrillFrame {
18
+ tableName: string;
19
+ filterTable?: string;
20
+ filterId?: string;
21
+ filterColumn?: string;
22
+ }
6
23
  /**
7
24
  * Is `frame` the same drill target as `other` (same table + same filter)?
8
25
  *
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The single, framework-free result-export utility for the shipped edit-db
3
+ * chain. Every export surface — the data grid, the raw SQL console result grid,
4
+ * and any future report runner — serializes through THIS module so there is
5
+ * exactly one CSV/JSON implementation in the shipped path.
6
+ *
7
+ * The serializers are matrix-based (`string[]` headers + `unknown[][]` rows) so
8
+ * they serve both the grid (record rows projected onto ordered fields) and the
9
+ * console (already-columnar positional rows) without a second code path.
10
+ *
11
+ * Value coercion is deliberate about the type classes that have bitten edit-db
12
+ * before:
13
+ * - NULL vs empty string are distinguishable in CSV (NULL -> empty field,
14
+ * "" -> a quoted empty pair, the PostgreSQL CSV convention).
15
+ * - bigint is rendered as its exact decimal string, never coerced through
16
+ * Number (which silently rounds past 2^53 — a known BigInt-PK bug class).
17
+ * - Date is rendered as a stable ISO-8601 string.
18
+ */
19
+ export type ExportFormat = 'csv' | 'json';
20
+ export type JsonMode = 'array' | 'lines';
21
+ /** The UTF-8 byte-order mark. Prepending it makes Excel open the CSV as UTF-8. */
22
+ export declare const UTF8_BOM = "\uFEFF";
23
+ /**
24
+ * Render one cell value as an RFC4180 CSV field.
25
+ *
26
+ * `null`/`undefined` become a bare empty field; an empty string becomes a
27
+ * quoted empty pair (`""`) so the two stay distinguishable in the output bytes.
28
+ * bigint and Date get exact/stable string forms; objects are JSON-encoded.
29
+ */
30
+ export declare function formatCsvCell(value: unknown): string;
31
+ export interface CsvOptions {
32
+ /** Prepend the UTF-8 BOM so Excel detects the encoding. Default false. */
33
+ bom?: boolean;
34
+ }
35
+ /**
36
+ * Serialize a header row plus data rows into an RFC4180 CSV string. Rows are
37
+ * joined with CRLF as the RFC specifies. `rows` are positional, aligned to
38
+ * `headers`.
39
+ */
40
+ export declare function buildCsv(headers: readonly string[], rows: ReadonlyArray<readonly unknown[]>, options?: CsvOptions): string;
41
+ export interface JsonOptions {
42
+ /** `array` (default): a single pretty-printed JSON array. `lines`: NDJSON. */
43
+ mode?: JsonMode;
44
+ }
45
+ /**
46
+ * Serialize header + rows into JSON, keyed by the header names. `array` mode
47
+ * emits one pretty-printed array; `lines` mode emits newline-delimited JSON
48
+ * (one compact object per line). bigint PKs round-trip losslessly as strings.
49
+ */
50
+ export declare function buildJson(headers: readonly string[], rows: ReadonlyArray<readonly unknown[]>, options?: JsonOptions): string;
51
+ /** One page of the source result set, as returned by the paging fetcher. */
52
+ export interface ExportPage {
53
+ /** Positional rows, aligned to the export headers. */
54
+ rows: unknown[][];
55
+ /** The server's total row count for the current filter (drives the loop). */
56
+ total: number;
57
+ }
58
+ export interface ExportAllOptions {
59
+ headers: string[];
60
+ format: ExportFormat;
61
+ /**
62
+ * Pull one page at the given offset. This is the ONLY data seam — the caller
63
+ * closes over the existing fetcher/query-builder so the current filters and
64
+ * sort are honored, and no new HTTP client is introduced.
65
+ */
66
+ fetchPage: (offset: number, limit: number) => Promise<ExportPage>;
67
+ /** Page size for each fetch. Default 500. */
68
+ pageSize?: number;
69
+ /** Hard stop after this many rows; the result is marked `truncated`. */
70
+ rowCap?: number;
71
+ /** Abort mid-export; the promise rejects and no content is assembled. */
72
+ signal?: AbortSignal;
73
+ /** Called after each page with the running fetched count and the total. */
74
+ onProgress?: (fetched: number, total: number) => void;
75
+ csv?: CsvOptions;
76
+ json?: JsonOptions;
77
+ }
78
+ export interface ExportResult {
79
+ content: string;
80
+ rowCount: number;
81
+ total: number;
82
+ truncated: boolean;
83
+ }
84
+ /**
85
+ * Drain the full result set page by page and serialize it once at the end.
86
+ *
87
+ * Cancellation is fail-safe: an aborted signal makes this throw before any
88
+ * content string is built, so the caller — which only writes a file on a
89
+ * resolved result — never produces a partial file. The row cap stops paging
90
+ * early and flags `truncated` so the caller can warn.
91
+ */
92
+ export declare function exportAllRows(options: ExportAllOptions): Promise<ExportResult>;
93
+ /**
94
+ * Default confirmation threshold: exporting more than this many rows prompts the
95
+ * user first, since it means paging the full result set out of the database.
96
+ */
97
+ export declare const DEFAULT_ROW_CAP = 100000;
98
+ /** Options passed to a bound {@link ExportRunner}. */
99
+ export interface RunExportOptions {
100
+ format: ExportFormat;
101
+ signal?: AbortSignal;
102
+ onProgress?: (fetched: number, total: number) => void;
103
+ rowCap?: number;
104
+ }
105
+ /**
106
+ * A surface-bound export runner: closes over the surface's headers + paging
107
+ * fetcher and drains the full result set. Returns null when the surface has
108
+ * nothing to export (no active query/table). This is the seam the grid toolbar
109
+ * and any other paged surface hand to the shared export button.
110
+ */
111
+ export type ExportRunner = (options: RunExportOptions) => Promise<ExportResult | null>;
112
+ /** MIME type for a downloaded file of the given format. */
113
+ export declare function mimeFor(format: ExportFormat): string;
114
+ /** `<base>-export.<ext>` filename for the given format. */
115
+ export declare function filenameFor(base: string, format: ExportFormat): string;
116
+ /**
117
+ * Trigger a browser download of `content` as `filename`. Builds a Blob, clicks a
118
+ * transient `<a download>`, then revokes the object URL. No-ops outside a DOM.
119
+ */
120
+ export declare function downloadTextFile(content: string, filename: string, mimeType: string): void;
121
+ /** Download a base64-encoded binary value as a file (image, PDF, any blob). */
122
+ export declare function downloadBinaryFile(base64: string, filename: string, mimeType: string): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,10 @@
1
1
  import { Column } from '../types/schema';
2
+ /**
3
+ * The temporal family of a column, from its DATABASE type (mirrors the server's
4
+ * ValidationRules.TemporalKindOf) — paramType can't carry this, because
5
+ * temporal mutation inputs are String on the wire.
6
+ */
7
+ export declare function temporalKindOf(column: Column): 'datetime' | 'date' | 'time' | undefined;
2
8
  /**
3
9
  * Anchors a pattern the same way the HTML5 `pattern` attribute and the server
4
10
  * validator do: wrap as ^(?:...)$ unless the author already anchored it with ^.
@@ -10,3 +16,13 @@ export declare function anchorPattern(pattern: string): string;
10
16
  * callers keep their own required policy (forms use !isNullable).
11
17
  */
12
18
  export declare function validateFieldValue(column: Column, value: unknown, isRequired: boolean): string | undefined;
19
+ /**
20
+ * Validates every value PRESENT in a mutation payload against its column's
21
+ * schema rules — the pre-send gate the mutation hooks run so EVERY write path
22
+ * (forms, programmatic callers, future surfaces) is checked, not only fields a
23
+ * form happened to attach validators to. Absent/cleared values are skipped:
24
+ * required-ness is form policy (a NOT NULL column with a DB default is legal to
25
+ * omit on insert), and this gate must never refuse what the server accepts.
26
+ * Returns all failures, or an empty array when the payload is clean.
27
+ */
28
+ export declare function validateRowValues(columns: readonly Column[], detail: Record<string, unknown>): string[];
package/dist/lib/fk.d.ts CHANGED
@@ -1,13 +1,34 @@
1
- import { Join, Table } from '../types/schema';
1
+ import { Column, Join, Table } from '../types/schema';
2
2
  export interface FkEqFilterResult {
3
3
  filterText: string;
4
4
  variables: Record<string, unknown>;
5
5
  params: string[];
6
6
  }
7
+ /**
8
+ * Coerce a raw cell/route value to the JS type matching a GraphQL scalar so the
9
+ * variable payload agrees with its declared type (e.g. a Boolean FK is sent as
10
+ * `true`, not `"true"`). Unparseable numerics are passed through untouched so
11
+ * the server reports the real error instead of receiving `NaN`.
12
+ */
13
+ export declare function coerceForGql(value: unknown, gqlType: string): unknown;
14
+ /** The base GraphQL scalar of a column (`paramType` minus non-null marker), defaulting to String. */
15
+ export declare function gqlTypeOf(column: Column | undefined): string;
7
16
  export declare function isFkMember(join: Join, columnName: string): boolean;
8
17
  export declare function isComposite(join: Join): boolean;
18
+ /**
19
+ * The destination column paired POSITIONALLY with `columnName` on the source side
20
+ * of `join`, or null when the pairing does not exist.
21
+ *
22
+ * Null is the whole point. The previous inline form was
23
+ * `destinationColumnNames[indexOf(name)] ?? destinationColumnNames[0]`: `indexOf`
24
+ * returns -1 for a column that is not part of the join, the indexed read is
25
+ * `undefined`, and `??` then substituted the FIRST destination column — so an
26
+ * unresolvable pairing silently produced a confident, wrong FK target instead of
27
+ * an absent one. Callers must handle null by declining the FK behaviour, never by
28
+ * guessing a column (see .claude/rules/composite-pk-compliance.md).
29
+ */
30
+ export declare function fkDestinationColumnFor(join: Join, columnName: string): string | null;
9
31
  export declare function findJoinBySource(joins: Join[], columnName: string): Join | undefined;
10
- export declare function findJoinByDestinationTable(joins: Join[], destinationTable: string): Join | undefined;
11
32
  /**
12
33
  * Build a `{_eq}`-per-column composite filter against the destination side of a join,
13
34
  * using values pulled from a source-side row. Single-column joins emit a single
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Saved-form definition shape consumed by the form runner.
3
+ *
4
+ * A {@link FormDefinition} is the builder's output (see the desktop shell's
5
+ * `forms/form-state.ts`): a bound single-record data-entry form over one table.
6
+ * The runner binds it to live data. The types are re-declared here — rather than
7
+ * imported from the shell — to keep this shipped stack self-contained (the same
8
+ * choice `common/saved-objects.ts` makes for its `SavedObject` contract), and to
9
+ * give the runner a tolerant parser for the `unknown` `SavedObject.definition`
10
+ * payload it is handed.
11
+ */
12
+ /** The data-entry widget a field renders as. Mirrors the builder's FormControlType. */
13
+ export type FormControlType = 'text' | 'textarea' | 'number' | 'checkbox' | 'date' | 'datetime' | 'select';
14
+ export interface FormField {
15
+ /** Column name within the bound table. */
16
+ column: string;
17
+ /** Display label shown beside the control. */
18
+ label: string;
19
+ control: FormControlType;
20
+ /** Read-only controls render disabled. */
21
+ readOnly: boolean;
22
+ /** Required marker (runtime enforces via field validation). */
23
+ required: boolean;
24
+ /** Whether the field appears on the form. */
25
+ include: boolean;
26
+ }
27
+ /** A schema-derived related-record section below the parent form. */
28
+ export interface FormSubform {
29
+ /** Stable relationship id, normally the parent collection field. */
30
+ relationship: string;
31
+ childTable: string;
32
+ /** Parent and child FK columns are parallel; composite links include every pair. */
33
+ parentColumns: string[];
34
+ childColumns: string[];
35
+ label: string;
36
+ mode: 'grid' | 'stacked';
37
+ }
38
+ export interface FormDefinition {
39
+ /** Identifier of the bound table (qualified "schema.name" or GraphQL name). */
40
+ table: string;
41
+ /** Form caption. */
42
+ title: string;
43
+ /** Layout columns (1–4): how many fields sit side-by-side. */
44
+ columns: number;
45
+ fields: FormField[];
46
+ subforms?: FormSubform[];
47
+ }
48
+ /**
49
+ * Validates an unknown value (e.g. a `SavedObject.definition`) as a
50
+ * {@link FormDefinition}. Returns null when it cannot be bound — a missing table,
51
+ * or no parseable fields, means there is nothing to render. Unparseable
52
+ * individual fields are dropped (tolerant), mirroring the builder's storage
53
+ * parser, so one bad field never blanks the whole form.
54
+ */
55
+ export declare function parseFormDefinition(value: unknown): FormDefinition | null;
56
+ /** The included fields, in definition order — what the runner renders. */
57
+ export declare function visibleFields(def: FormDefinition): FormField[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pure record-cursor math for the form runner's first/prev/next/last navigation.
3
+ *
4
+ * The runner browses a table one record at a time by absolute offset (a
5
+ * `limit: 1 offset: <index>` fetch), so the only stateful arithmetic is moving a
6
+ * bounded index across `[0, total)`. Keeping it here makes it unit-testable
7
+ * without a fetcher or DOM, and keeps the component thin.
8
+ */
9
+ export type NavDirection = 'first' | 'prev' | 'next' | 'last';
10
+ /**
11
+ * Resolves the target absolute index for a navigation step, clamped into the
12
+ * valid range. `total <= 0` (empty table) always resolves to 0 so callers show
13
+ * the "no records" state rather than a negative offset.
14
+ */
15
+ export declare function nextIndex(current: number, total: number, direction: NavDirection): number;
16
+ /** Whether a navigation step from `current` would actually move (false at a bound). */
17
+ export declare function canNavigate(current: number, total: number, direction: NavDirection): boolean;
18
+ /** Human 1-based position label, e.g. "3 of 12". Empty table reads "0 of 0". */
19
+ export declare function positionLabel(index: number, total: number): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { Join, Table } from '../types/schema';
2
+ export interface ChildFilterPlan {
3
+ params: string[];
4
+ filter: string;
5
+ variables: Record<string, unknown>;
6
+ }
7
+ /** True only when every parent-key/FK pair can be safely bound. */
8
+ export declare function relationshipIsUsable(parent: Table, child: Table, join: Join): boolean;
9
+ /** Builds a parameterized child filter; composite relationships bind every pair. */
10
+ export declare function childFilterPlan(parent: Table, child: Table, join: Join, row: Record<string, unknown> | null): ChildFilterPlan | null;
11
+ /** Values supplied to a child create form; callers hide these FK controls. */
12
+ export declare function childPrefill(join: Join, row: Record<string, unknown> | null): Record<string, unknown>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { FormControlType, FormField } from './form-definition';
2
+ /** Structural subset of the app-metadata `FieldMetadata` this module reads. */
3
+ export interface FieldWidgetHint {
4
+ /** Widget hint (e.g. `text`, `select`) — overrides the saved control when recognised. */
5
+ widget?: string;
6
+ /** Whether the field is visible. Metadata default is `true`. */
7
+ visible?: boolean;
8
+ /** Whether the field is read-only. Metadata default is `false`. */
9
+ readOnly?: boolean;
10
+ }
11
+ /** The rendered control plus its resolved read-only / visible state. */
12
+ export interface ResolvedWidget {
13
+ control: FormControlType;
14
+ readOnly: boolean;
15
+ visible: boolean;
16
+ }
17
+ /**
18
+ * Resolves the effective widget for a field. The metadata widget hint (when it
19
+ * names a known control) wins over the saved control; read-only is the union of
20
+ * both sources (either can lock a field, neither can unlock what the other
21
+ * locked); visibility requires the field be included AND not hidden by metadata.
22
+ */
23
+ export declare function resolveWidget(field: FormField, meta?: FieldWidgetHint): ResolvedWidget;
24
+ /** How a control maps onto a DOM element for rendering. */
25
+ export type ControlRender = {
26
+ kind: 'input';
27
+ type: string;
28
+ } | {
29
+ kind: 'textarea';
30
+ } | {
31
+ kind: 'checkbox';
32
+ } | {
33
+ kind: 'select';
34
+ };
35
+ /** Maps a control type to the element the runner should render. */
36
+ export declare function controlRender(control: FormControlType): ControlRender;
@@ -0,0 +1 @@
1
+ export {};
@@ -51,3 +51,29 @@ export declare function formatRelative(date: Date, now?: Date): string;
51
51
  * `title` revealing the exact value), or the plain string otherwise.
52
52
  */
53
53
  export declare function formatColumnValue(value: unknown, column: Column, options?: FormatOptions): ReactNode;
54
+ /**
55
+ * Format a raw date/datetime string for grid-cell display via toLocaleString,
56
+ * blanking sentinel "unset" instants.
57
+ *
58
+ * Only exact placeholder/sentinel instants are blanked, not legitimate
59
+ * historical dates. A broad "before 1973" cutoff hid real dates (e.g. a 1900
60
+ * birthdate or a 1969 record). The two markers that actually mean "unset":
61
+ * the Unix epoch (timestamp 0, i.e. 1970-01-01T00:00:00Z) and .NET/SQL
62
+ * DateTime.MinValue (year 0001-01-01).
63
+ */
64
+ export declare function formatDateCellValue(value: string): string;
65
+ /**
66
+ * Formats a number into an abbreviated string representation.
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * abbreviateNumber(12) // "12"
71
+ * abbreviateNumber(1200) // "1.2k"
72
+ * abbreviateNumber(45000) // "45k"
73
+ * abbreviateNumber(1200000) // "1.2M"
74
+ * ```
75
+ *
76
+ * @param num - Number to abbreviate
77
+ * @returns Abbreviated string representation
78
+ */
79
+ export declare function abbreviateNumber(num: number | null): string;
@@ -0,0 +1,63 @@
1
+ import { ColumnFiltersState } from '@tanstack/react-table';
2
+ import { Column, Table } from '../types/schema';
3
+ /** URL parameter that owns the selected grouping field. */
4
+ export declare const GRID_GROUP_BY_PARAM = "gb";
5
+ /** Optional, URL-owned numeric measure shown beside every server aggregate. */
6
+ export declare const GRID_GROUP_SUM_PARAM = "gs";
7
+ /**
8
+ * Resolves the URL value against the current schema. Keeping this lookup at
9
+ * the boundary means a stale browser-history value can never become GraphQL
10
+ * source text for a different table.
11
+ */
12
+ export declare function groupingColumnFromUrl(value: string | null, table: Table | null): Column | null;
13
+ /** The aggregate result is deliberately server-owned; it is never calculated from page rows. */
14
+ export interface GroupingRow {
15
+ value: unknown;
16
+ count: number;
17
+ sum: unknown;
18
+ }
19
+ /**
20
+ * Grouped results have their own aggregate-row ordering. It is intentionally
21
+ * separate from the flat-grid sort: the latter orders individual records,
22
+ * while this controls aggregate buckets. Count ties always use the group key
23
+ * ascending so pagination remains deterministic.
24
+ */
25
+ export type GroupingSort = {
26
+ field: 'key' | 'count';
27
+ desc: boolean;
28
+ };
29
+ export declare const DEFAULT_GROUPING_SORT: GroupingSort;
30
+ export interface GridGroupingRequest {
31
+ query: string;
32
+ variables: Record<string, unknown>;
33
+ groupBy: Column;
34
+ sumBy: Column | null;
35
+ sort: GroupingSort;
36
+ }
37
+ export interface GridGroupMemberRequest {
38
+ query: string;
39
+ variables: Record<string, unknown>;
40
+ /** Response field selected from the current schema, never parsed from query text. */
41
+ responseKey: string;
42
+ }
43
+ export declare function groupingSumColumnFromUrl(value: string | null, table: Table | null): Column | null;
44
+ /**
45
+ * Update one of the grouping-owned URL fields without disturbing filters, the
46
+ * selected profile, or other application-owned state. The caller receives a
47
+ * fresh instance so it is safe to use during render/effects as well as event
48
+ * handlers.
49
+ */
50
+ export declare function withGroupingUrlParam(search: URLSearchParams, param: typeof GRID_GROUP_BY_PARAM | typeof GRID_GROUP_SUM_PARAM, value: string | null): URLSearchParams;
51
+ /** Remove grouping state when navigation changes the source table. */
52
+ export declare function withoutGroupingUrlParams(search: URLSearchParams): URLSearchParams;
53
+ /** Builds the schema-derived aggregate document used by the grouped grid. */
54
+ export declare function buildGridGroupingRequest(table: Table, groupBy: Column, columnFilters: ColumnFiltersState, headerFilter: string, sumBy?: Column | null, sort?: GroupingSort): GridGroupingRequest;
55
+ export declare function readGroupingRows(data: Record<string, unknown> | undefined, table: Table, groupBy: Column, sort?: GroupingSort): GroupingRow[];
56
+ /** Defined aggregate ordering for the server-grouped replacement surface. */
57
+ export declare function compareGroupingRowsByKey(left: GroupingRow, right: GroupingRow): number;
58
+ /** Sort aggregate buckets by the selected key/count direction with a stable key tie-breaker. */
59
+ export declare function compareGroupingRows(left: GroupingRow, right: GroupingRow, sort?: GroupingSort): number;
60
+ /** Reads a selected configured measure without ever deriving it from page rows. */
61
+ export declare function readGroupingRowsWithSum(data: Record<string, unknown> | undefined, table: Table, groupBy: Column, sumBy: Column | null, sort?: GroupingSort): GroupingRow[];
62
+ /** Schema-derived member query for a single aggregate key. It merges, never replaces, active filters. */
63
+ export declare function buildGridGroupMemberRequest(table: Table, groupBy: Column, value: unknown, columnFilters: ColumnFiltersState, headerFilter: string, sort?: readonly string[]): GridGroupMemberRequest;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+ /**
3
+ * Invalidate every query family that can serve rows affected by a write to
4
+ * `tableName`. Returns the refetch promise for callers that want to await it;
5
+ * queries are marked invalidated synchronously either way.
6
+ */
7
+ export declare function invalidateAfterTableWrite(queryClient: QueryClient, tableName: string): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @module lib/label-match
3
+ *
4
+ * Client-side label filter shared by FK dropdowns (`ParentField`) and the
5
+ * many-to-many `TargetPicker` fallback. Used when the label column is not
6
+ * String-typed, so the server-side `_contains` search is unavailable and rows
7
+ * are filtered within the fetched window instead.
8
+ */
9
+ /**
10
+ * Case-insensitive substring match against a row's `label`, falling back to its
11
+ * id field (`idKey`) when no label was fetched.
12
+ */
13
+ export declare function matchesLabel(row: object, idKey: string, term: string): boolean;
@@ -0,0 +1 @@
1
+ export {};
package/dist/lib/m2m.d.ts CHANGED
@@ -31,13 +31,45 @@ export declare function m2mRowsQuery(junction: Table, target: Table, m2m: ManyTo
31
31
  };
32
32
  export interface M2mTargetPickerPlan {
33
33
  query: string;
34
- idColumn: string;
34
+ /**
35
+ * Columns that together identify a target row — every PK column in declaration
36
+ * order. Both the picker and the already-linked set must key on ALL of them
37
+ * (via {@link m2mTargetIdentityColumns}) or two rows sharing their first key
38
+ * column collapse into one.
39
+ */
40
+ identityColumns: string[];
41
+ /**
42
+ * The target column the junction FK references — the value written on attach.
43
+ * Deliberately separate from identity: on a composite-PK target, or a junction
44
+ * that points at a unique non-PK column, the two are different columns.
45
+ */
46
+ linkColumn: string;
47
+ /** True when the label column is String-typed and thus supports the
48
+ * server-side `_contains` search; callers fall back to client filtering. */
49
+ serverSearch: boolean;
35
50
  }
51
+ /**
52
+ * The columns that identify a target row, in declaration order.
53
+ *
54
+ * Single source of truth for BOTH sides of the picker's duplicate check — the
55
+ * fetched target rows and the already-linked rows read off the junction. Keying
56
+ * the two independently (or on the first PK column alone) let two distinct composite-PK
57
+ * rows that share their first key column collapse: one was wrongly marked
58
+ * "Already linked" and its button disabled, making a real target unpickable.
59
+ *
60
+ * Falls back to the junction's target columns when the target declares no primary
61
+ * key, which is the only identity available in that case.
62
+ */
63
+ export declare function m2mTargetIdentityColumns(target: Table, m2m: ManyToManyJoin): string[];
36
64
  /**
37
65
  * Build the target-list query used when adding a many-to-many link.
38
66
  * Sort values are BifrostQL enum literals, so they must not be quoted.
67
+ *
68
+ * When `search` is non-empty the list is filtered server-side (a `_contains` on
69
+ * the label column) so a large target table stays findable beyond the fetch
70
+ * limit, instead of searching only within a client-side window.
39
71
  */
40
- export declare function m2mTargetPickerPlan(target: Table, m2m: ManyToManyJoin): M2mTargetPickerPlan;
72
+ export declare function m2mTargetPickerPlan(target: Table, m2m: ManyToManyJoin, search?: string): M2mTargetPickerPlan;
41
73
  /**
42
74
  * Read the target row's id and display label from a junction row's nested target
43
75
  * selection (emitted by {@link m2mRowsQuery}). Returns null when the link has no
@@ -53,9 +85,16 @@ export declare function targetDisplay(junctionRow: Record<string, unknown>, m2m:
53
85
  * the chosen target key on the junction target FK. Any payload columns are left
54
86
  * unset so the database applies its defaults; the user edits them afterwards.
55
87
  *
56
- * Single-column junction FKs only. `parentId`/`targetId` are route-encoded
57
- * strings; for a composite FK they carry multiple values joined by "::", which
58
- * would be written verbatim into one column — silent data corruption. Rather
59
- * than guess a decomposition, fail loudly until composite-FK support is built.
88
+ * Single-column junction FKs only. `parentId` is a route-encoded string
89
+ * (produced by rowIdOf/encodePkRoute); for a composite FK it carries multiple
90
+ * values joined by "::", which would be written verbatim into one column —
91
+ * silent data corruption. Rather than guess a decomposition, fail loudly until
92
+ * composite-FK support is built.
93
+ *
94
+ * The parent value is percent-DECODED before being written: the read side
95
+ * (m2mRowsQuery) decodes the route id before filtering, so writing the encoded
96
+ * form (e.g. 'caf%C3%A9' for a 'café' key) would insert a dangling FK that the
97
+ * panel's filter never matches. `targetId` is NOT route-encoded — it comes raw
98
+ * from the picker row — so it is written as-is.
60
99
  */
61
100
  export declare function attachJunctionDetail(m2m: ManyToManyJoin, parentId: string, targetId: string): Record<string, unknown>;