@jxsuite/connector 0.6.1 → 0.6.3
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/LICENSE +1 -1
- package/package.json +4 -4
- package/src/columns.ts +28 -21
- package/src/connectors.ts +19 -8
- package/src/d1.ts +18 -14
- package/src/ddl.ts +8 -14
- package/src/dialects/bun-sqlite.ts +11 -0
- package/src/dialects/d1-http.ts +22 -6
- package/src/provider-utils.ts +28 -14
- package/src/query.ts +40 -23
- package/src/sqlite.ts +8 -6
- package/src/supabase.ts +10 -12
- package/src/table-actions.ts +34 -20
- package/src/table-delete.ts +2 -1
- package/src/table-insert.ts +2 -1
- package/src/table-node.ts +22 -21
- package/src/table-shared.ts +22 -18
- package/src/table-state.ts +33 -16
- package/src/table-update.ts +2 -1
- package/src/types.ts +30 -23
- package/src/validate.ts +25 -26
- package/src/worker.ts +128 -76
package/src/sqlite.ts
CHANGED
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
* Supabase.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
10
11
|
import { createBunSqliteDialect } from "./dialects/bun-sqlite.ts";
|
|
11
12
|
import { deployWithDialect, testWithDialect } from "./provider-utils.ts";
|
|
12
13
|
import type { Dialect } from "kysely";
|
|
13
|
-
import type { ConnectorProvider } from "./provider-utils.ts";
|
|
14
|
+
import type { ConnectorProvider, HostEnv } from "./provider-utils.ts";
|
|
14
15
|
import type { ConnectionDef } from "./types.ts";
|
|
15
16
|
|
|
16
17
|
/** Resolve the SQLite file path for a connection. */
|
|
17
|
-
export function sqliteFilePath(connection: ConnectionDef, env:
|
|
18
|
-
const
|
|
18
|
+
export function sqliteFilePath(connection: ConnectionDef, env: HostEnv): string {
|
|
19
|
+
const projectRoot = env["JX_PROJECT_ROOT"];
|
|
20
|
+
const root = isString(projectRoot) ? projectRoot : ".";
|
|
19
21
|
const name = connection.$name ?? "default";
|
|
20
22
|
const file = connection.file ?? `./.jx/data/${name}.sqlite`;
|
|
21
23
|
if (file === ":memory:" || file.startsWith("/")) {
|
|
@@ -25,7 +27,7 @@ export function sqliteFilePath(connection: ConnectionDef, env: Record<string, un
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** Build the bun:sqlite dialect for a connection. */
|
|
28
|
-
function sqliteDialect(connection: ConnectionDef, env:
|
|
30
|
+
function sqliteDialect(connection: ConnectionDef, env: HostEnv): Dialect {
|
|
29
31
|
return createBunSqliteDialect({ url: sqliteFilePath(connection, env) });
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -34,14 +36,14 @@ export const Sqlite = {
|
|
|
34
36
|
deploySchema: async (
|
|
35
37
|
tables: Parameters<ConnectorProvider["deploySchema"]>[0],
|
|
36
38
|
connection: ConnectionDef,
|
|
37
|
-
options: { env:
|
|
39
|
+
options: { env: HostEnv; dryRun?: boolean },
|
|
38
40
|
) => {
|
|
39
41
|
const dialect = sqliteDialect(connection, options.env);
|
|
40
42
|
return deployWithDialect(dialect, "sqlite", tables, { dryRun: options.dryRun ?? false });
|
|
41
43
|
},
|
|
42
44
|
dialect: sqliteDialect,
|
|
43
45
|
kind: "sqlite",
|
|
44
|
-
testConnection: async (connection: ConnectionDef, env:
|
|
46
|
+
testConnection: async (connection: ConnectionDef, env: HostEnv) => {
|
|
45
47
|
try {
|
|
46
48
|
return await testWithDialect(sqliteDialect(connection, env));
|
|
47
49
|
} catch (error) {
|
package/src/supabase.ts
CHANGED
|
@@ -8,29 +8,27 @@
|
|
|
8
8
|
* never touch a postgres connection never import them.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { isNonNullObject, isString } from "@jxsuite/schema/guards";
|
|
11
12
|
import { deployWithDialect, testWithDialect } from "./provider-utils.ts";
|
|
12
13
|
import type { Dialect } from "kysely";
|
|
13
|
-
import type { ConnectorProvider } from "./provider-utils.ts";
|
|
14
|
+
import type { ConnectorProvider, HostEnv } from "./provider-utils.ts";
|
|
14
15
|
import type { ConnectionDef } from "./types.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Resolve the postgres URL for a connection: Hyperdrive binding first, then `urlEnv`.
|
|
18
19
|
*
|
|
19
20
|
* @param {ConnectionDef} connection
|
|
20
|
-
* @param {
|
|
21
|
+
* @param {HostEnv} env
|
|
21
22
|
* @returns {string}
|
|
22
23
|
*/
|
|
23
|
-
export function resolvePostgresUrl(
|
|
24
|
-
connection: ConnectionDef,
|
|
25
|
-
env: Record<string, unknown>,
|
|
26
|
-
): string {
|
|
24
|
+
export function resolvePostgresUrl(connection: ConnectionDef, env: HostEnv): string {
|
|
27
25
|
const { binding, urlEnv, $name = "connection" } = connection;
|
|
28
|
-
const bound = binding ?
|
|
29
|
-
if (bound &&
|
|
26
|
+
const bound = binding ? env[binding] : undefined;
|
|
27
|
+
if (isNonNullObject(bound) && "connectionString" in bound && isString(bound.connectionString)) {
|
|
30
28
|
return bound.connectionString;
|
|
31
29
|
}
|
|
32
30
|
const url = urlEnv ? env[urlEnv] : undefined;
|
|
33
|
-
if (
|
|
31
|
+
if (isString(url) && url !== "") {
|
|
34
32
|
return url;
|
|
35
33
|
}
|
|
36
34
|
throw new Error(
|
|
@@ -62,15 +60,15 @@ export const Supabase = {
|
|
|
62
60
|
deploySchema: async (
|
|
63
61
|
tables: Parameters<ConnectorProvider["deploySchema"]>[0],
|
|
64
62
|
connection: ConnectionDef,
|
|
65
|
-
options: { env:
|
|
63
|
+
options: { env: HostEnv; dryRun?: boolean },
|
|
66
64
|
) => {
|
|
67
65
|
const dialect = await postgresDialect(resolvePostgresUrl(connection, options.env));
|
|
68
66
|
return deployWithDialect(dialect, "postgres", tables, { dryRun: options.dryRun ?? false });
|
|
69
67
|
},
|
|
70
|
-
dialect: (connection: ConnectionDef, env:
|
|
68
|
+
dialect: (connection: ConnectionDef, env: HostEnv) =>
|
|
71
69
|
postgresDialect(resolvePostgresUrl(connection, env)),
|
|
72
70
|
kind: "postgres",
|
|
73
|
-
testConnection: async (connection: ConnectionDef, env:
|
|
71
|
+
testConnection: async (connection: ConnectionDef, env: HostEnv) => {
|
|
74
72
|
try {
|
|
75
73
|
const dialect = await postgresDialect(resolvePostgresUrl(connection, env));
|
|
76
74
|
return await testWithDialect(dialect);
|
package/src/table-actions.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* an equivalent real function.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { isFunction, isString } from "@jxsuite/schema/guards";
|
|
11
12
|
import { jsObjectSource, jsValueSource, resolveIdValue } from "./table-shared.ts";
|
|
13
|
+
import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
|
|
12
14
|
import type { LowerContext } from "./table-state.ts";
|
|
13
15
|
|
|
14
16
|
export type ActionMethod = "POST" | "PATCH" | "DELETE";
|
|
@@ -16,13 +18,23 @@ export type ActionMethod = "POST" | "PATCH" | "DELETE";
|
|
|
16
18
|
/** The def shape all three action classes accept. */
|
|
17
19
|
export interface ActionDef {
|
|
18
20
|
table?: string;
|
|
19
|
-
id?:
|
|
21
|
+
id?: JsonValue;
|
|
20
22
|
/** Literal payload columns merged over the submitting form's FormData. */
|
|
21
|
-
values?:
|
|
23
|
+
values?: JsonObject;
|
|
22
24
|
_document?: { route?: { _pathParams?: Record<string, string> } };
|
|
23
|
-
[key: string]: unknown;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The live state scope an action handler runs against (interpreter path): `${...}` values are
|
|
29
|
+
* evaluated against it as `state`, and a successful write bumps its `_v` read-after-write counter.
|
|
30
|
+
*/
|
|
31
|
+
export interface ActionScope {
|
|
32
|
+
_v?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A `values` entry compiled to a template function over the live state scope. */
|
|
36
|
+
type ValueTemplate = (state: ActionScope) => string;
|
|
37
|
+
|
|
26
38
|
/** Build the /_jx/data URL JS-source expression for a lowered action. */
|
|
27
39
|
function urlSource(method: ActionMethod, def: ActionDef, context: LowerContext): string {
|
|
28
40
|
const table = def.table ?? "";
|
|
@@ -39,13 +51,13 @@ function urlSource(method: ActionMethod, def: ActionDef, context: LowerContext):
|
|
|
39
51
|
* @param {ActionMethod} method
|
|
40
52
|
* @param {ActionDef} def
|
|
41
53
|
* @param {LowerContext} [context]
|
|
42
|
-
* @returns {
|
|
54
|
+
* @returns {JsonObject} A core Function def with an `event` parameter
|
|
43
55
|
*/
|
|
44
56
|
export function lowerActionDef(
|
|
45
57
|
method: ActionMethod,
|
|
46
58
|
def: ActionDef,
|
|
47
59
|
context: LowerContext = {},
|
|
48
|
-
):
|
|
60
|
+
): JsonObject {
|
|
49
61
|
const url = urlSource(method, def, context);
|
|
50
62
|
const lines: string[] = [
|
|
51
63
|
"if (event && typeof event.preventDefault === 'function') event.preventDefault();",
|
|
@@ -82,17 +94,14 @@ export function lowerActionDef(
|
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
/** Evaluate a `${...}`-bearing values object against a live scope (interpreter path). */
|
|
85
|
-
function evaluateValues(
|
|
86
|
-
|
|
87
|
-
scope: Record<string, unknown>,
|
|
88
|
-
): Record<string, unknown> {
|
|
89
|
-
const out: Record<string, unknown> = {};
|
|
97
|
+
function evaluateValues(values: JsonObject | undefined, scope: ActionScope): JsonObject {
|
|
98
|
+
const out: JsonObject = {};
|
|
90
99
|
for (const [key, value] of Object.entries(values ?? {})) {
|
|
91
|
-
if (
|
|
100
|
+
if (isString(value) && value.includes("${")) {
|
|
92
101
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
102
|
+
/* SAFETY: the body is a single `return` of a template literal, so the compiled function
|
|
103
|
+
takes the state scope as its one parameter and always returns a string. */
|
|
104
|
+
const fn = new Function("state", `return \`${value}\`;`) as ValueTemplate;
|
|
96
105
|
out[key] = fn(scope);
|
|
97
106
|
} catch {
|
|
98
107
|
out[key] = value;
|
|
@@ -109,12 +118,12 @@ function evaluateValues(
|
|
|
109
118
|
*
|
|
110
119
|
* @param {ActionMethod} method
|
|
111
120
|
* @param {ActionDef} config
|
|
112
|
-
* @returns {(scope:
|
|
113
|
-
*
|
|
121
|
+
* @returns {(scope: ActionScope, event?: Event) => Promise<boolean>} Resolves true when the write
|
|
122
|
+
* succeeded (and `scope._v` was bumped)
|
|
114
123
|
*/
|
|
115
124
|
export function makeActionHandler(method: ActionMethod, config: ActionDef) {
|
|
116
|
-
return async (scope:
|
|
117
|
-
if (event &&
|
|
125
|
+
return async (scope: ActionScope, event?: Event): Promise<boolean> => {
|
|
126
|
+
if (event && isFunction(event.preventDefault)) {
|
|
118
127
|
event.preventDefault();
|
|
119
128
|
}
|
|
120
129
|
const table = config.table ?? "";
|
|
@@ -129,10 +138,15 @@ export function makeActionHandler(method: ActionMethod, config: ActionDef) {
|
|
|
129
138
|
if (method === "DELETE") {
|
|
130
139
|
init = { method };
|
|
131
140
|
} else {
|
|
141
|
+
/* SAFETY: the handler is bound as an element's event listener (onsubmit/onclick), so the
|
|
142
|
+
event's target is the HTML element that dispatched it; `form` is read optionally because
|
|
143
|
+
only form-associated controls carry it. */
|
|
132
144
|
const target = event?.target as
|
|
133
145
|
| (HTMLElement & { form?: HTMLFormElement | null })
|
|
134
146
|
| null
|
|
135
147
|
| undefined;
|
|
148
|
+
/* SAFETY: Element.tagName reads "FORM" exactly for the HTML <form> element (HTML-namespace
|
|
149
|
+
tag names are reported uppercase), which is an HTMLFormElement. */
|
|
136
150
|
form =
|
|
137
151
|
target && target.tagName === "FORM" ? (target as HTMLFormElement) : (target?.form ?? null);
|
|
138
152
|
const formData = form ? Object.fromEntries(new FormData(form)) : {};
|
|
@@ -146,10 +160,10 @@ export function makeActionHandler(method: ActionMethod, config: ActionDef) {
|
|
|
146
160
|
|
|
147
161
|
const response = await fetch(url, init);
|
|
148
162
|
if (response.ok) {
|
|
149
|
-
if (method === "POST" && form &&
|
|
163
|
+
if (method === "POST" && form && isFunction(form.reset)) {
|
|
150
164
|
form.reset();
|
|
151
165
|
}
|
|
152
|
-
scope._v = (
|
|
166
|
+
scope._v = (scope._v || 0) + 1;
|
|
153
167
|
}
|
|
154
168
|
return response.ok;
|
|
155
169
|
};
|
package/src/table-delete.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { lowerActionDef, makeActionHandler } from "./table-actions.ts";
|
|
9
|
+
import type { JsonObject } from "@jxsuite/schema/types";
|
|
9
10
|
import type { ActionDef } from "./table-actions.ts";
|
|
10
11
|
import type { LowerContext } from "./table-state.ts";
|
|
11
12
|
|
|
@@ -22,7 +23,7 @@ export class TableDelete {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/** Static `lower` capability: compile away into an inline Function def. */
|
|
25
|
-
static lower(def: ActionDef, context: LowerContext = {}):
|
|
26
|
+
static lower(def: ActionDef, context: LowerContext = {}): JsonObject {
|
|
26
27
|
return lowerActionDef("DELETE", def, context);
|
|
27
28
|
}
|
|
28
29
|
}
|
package/src/table-insert.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { lowerActionDef, makeActionHandler } from "./table-actions.ts";
|
|
10
|
+
import type { JsonObject } from "@jxsuite/schema/types";
|
|
10
11
|
import type { ActionDef } from "./table-actions.ts";
|
|
11
12
|
import type { LowerContext } from "./table-state.ts";
|
|
12
13
|
|
|
@@ -23,7 +24,7 @@ export class TableInsert {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** Static `lower` capability: compile away into an inline Function def. */
|
|
26
|
-
static lower(def: ActionDef, context: LowerContext = {}):
|
|
27
|
+
static lower(def: ActionDef, context: LowerContext = {}): JsonObject {
|
|
27
28
|
return lowerActionDef("POST", def, context);
|
|
28
29
|
}
|
|
29
30
|
}
|
package/src/table-node.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* empty results.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { isNumber } from "@jxsuite/schema/guards";
|
|
11
12
|
import { Kysely } from "kysely";
|
|
12
13
|
import { fromStorage, planTable } from "./columns.ts";
|
|
13
14
|
import { resolveDialect } from "./connectors.ts";
|
|
@@ -15,20 +16,23 @@ import { syncTables } from "./ddl.ts";
|
|
|
15
16
|
import { applyFilter, applySort, normalizeFilter, normalizeSort } from "./query.ts";
|
|
16
17
|
import { tablesForConnection } from "./provider-utils.ts";
|
|
17
18
|
import type { SqlDialectKind } from "./columns.ts";
|
|
19
|
+
import type { JsonValue } from "@jxsuite/schema/types";
|
|
18
20
|
import type { ConnectionsConfig } from "./connectors.ts";
|
|
19
|
-
import type { DynamicDatabase } from "./query.ts";
|
|
21
|
+
import type { DynamicDatabase, DynamicRow } from "./query.ts";
|
|
20
22
|
import type { TableQueryDef } from "./table-shared.ts";
|
|
21
23
|
import type { TableDef } from "./types.ts";
|
|
22
24
|
|
|
25
|
+
/** The project context hosts inject under `_project`: its config, root, and data section. */
|
|
26
|
+
export interface TableProjectContext {
|
|
27
|
+
config?: ConnectionsConfig & { data?: Record<string, TableDef> };
|
|
28
|
+
root?: string;
|
|
29
|
+
data?: Record<string, TableDef>;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
/** The `_project`-bearing config shape hosts inject into state classes. */
|
|
24
33
|
export interface TableNodeConfig extends TableQueryDef {
|
|
25
|
-
id?:
|
|
26
|
-
_project?:
|
|
27
|
-
config?: ConnectionsConfig & { data?: Record<string, TableDef> };
|
|
28
|
-
root?: string;
|
|
29
|
-
data?: Record<string, TableDef>;
|
|
30
|
-
[key: string]: unknown;
|
|
31
|
-
};
|
|
34
|
+
id?: JsonValue;
|
|
35
|
+
_project?: TableProjectContext;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
interface OpenedTable {
|
|
@@ -48,10 +52,10 @@ async function openTable(config: TableNodeConfig): Promise<OpenedTable> {
|
|
|
48
52
|
if (!tableDef) {
|
|
49
53
|
throw new Error(`unknown table "${tableName}" (no data section entry)`);
|
|
50
54
|
}
|
|
51
|
-
const env
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
+
const env = { ...process.env };
|
|
56
|
+
if (project.root !== undefined) {
|
|
57
|
+
env["JX_PROJECT_ROOT"] = project.root;
|
|
58
|
+
}
|
|
55
59
|
const { dialect, type } = await resolveDialect(tableDef.connection, projectConfig, env);
|
|
56
60
|
const db = new Kysely<DynamicDatabase>({ dialect });
|
|
57
61
|
const connection = projectConfig.connections?.[tableDef.connection];
|
|
@@ -66,19 +70,19 @@ async function openTable(config: TableNodeConfig): Promise<OpenedTable> {
|
|
|
66
70
|
* Query a table with the def's filter/sort/limit/offset (the content grammar).
|
|
67
71
|
*
|
|
68
72
|
* @param {TableNodeConfig} config
|
|
69
|
-
* @returns {Promise<
|
|
73
|
+
* @returns {Promise<DynamicRow[]>}
|
|
70
74
|
*/
|
|
71
|
-
export async function queryTable(config: TableNodeConfig): Promise<
|
|
75
|
+
export async function queryTable(config: TableNodeConfig): Promise<DynamicRow[]> {
|
|
72
76
|
const { db, kind, tables, tableName } = await openTable(config);
|
|
73
77
|
try {
|
|
74
78
|
const plan = planTable(tableName, tables[tableName]!, tables, kind);
|
|
75
79
|
let qb = db.selectFrom(tableName).selectAll();
|
|
76
80
|
qb = applyFilter(qb, normalizeFilter(config.filter), plan.columns);
|
|
77
81
|
qb = applySort(qb, normalizeSort(config.sort), plan.columns);
|
|
78
|
-
if (
|
|
82
|
+
if (isNumber(config.limit) && config.limit > 0) {
|
|
79
83
|
qb = qb.limit(config.limit);
|
|
80
84
|
}
|
|
81
|
-
if (
|
|
85
|
+
if (isNumber(config.offset) && config.offset > 0) {
|
|
82
86
|
qb = qb.limit(config.limit ?? 100_000_000).offset(config.offset);
|
|
83
87
|
}
|
|
84
88
|
const rows = await qb.execute();
|
|
@@ -93,12 +97,9 @@ export async function queryTable(config: TableNodeConfig): Promise<Record<string
|
|
|
93
97
|
*
|
|
94
98
|
* @param {TableNodeConfig} config
|
|
95
99
|
* @param {string} id
|
|
96
|
-
* @returns {Promise<
|
|
100
|
+
* @returns {Promise<DynamicRow | null>}
|
|
97
101
|
*/
|
|
98
|
-
export async function getEntry(
|
|
99
|
-
config: TableNodeConfig,
|
|
100
|
-
id: string,
|
|
101
|
-
): Promise<Record<string, unknown> | null> {
|
|
102
|
+
export async function getEntry(config: TableNodeConfig, id: string): Promise<DynamicRow | null> {
|
|
102
103
|
const { db, kind, tables, tableName } = await openTable(config);
|
|
103
104
|
try {
|
|
104
105
|
const plan = planTable(tableName, tables[tableName]!, tables, kind);
|
package/src/table-shared.ts
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* machinery.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { isJsonObject, isNumber, isRef, isString } from "@jxsuite/schema/guards";
|
|
16
|
+
import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
|
|
17
|
+
|
|
15
18
|
/** A `${...}` template span (Jx binding syntax preserved through lowering). */
|
|
16
19
|
const TEMPLATE_SPAN = /(\$\{[^}]*\})/;
|
|
17
20
|
|
|
@@ -32,12 +35,13 @@ export function encodeQueryValue(value: string): string {
|
|
|
32
35
|
/** The wire-facing query options a TableQuery/TableEntry def may carry. */
|
|
33
36
|
export interface TableQueryDef {
|
|
34
37
|
table?: string;
|
|
35
|
-
filter
|
|
36
|
-
|
|
38
|
+
/** The content filter grammar: a rule array or an equality-object shorthand. */
|
|
39
|
+
filter?: JsonValue;
|
|
40
|
+
/** One `{ field, order }` sort rule or a list of them. */
|
|
41
|
+
sort?: JsonValue;
|
|
37
42
|
limit?: number;
|
|
38
43
|
offset?: number;
|
|
39
44
|
include?: string | string[];
|
|
40
|
-
[key: string]: unknown;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/**
|
|
@@ -60,10 +64,10 @@ export function buildDataUrl(
|
|
|
60
64
|
if (def.sort !== undefined && def.sort !== null) {
|
|
61
65
|
parts.push(`sort=${encodeQueryValue(JSON.stringify(def.sort))}`);
|
|
62
66
|
}
|
|
63
|
-
if (
|
|
67
|
+
if (isNumber(def.limit)) {
|
|
64
68
|
parts.push(`limit=${def.limit}`);
|
|
65
69
|
}
|
|
66
|
-
if (
|
|
70
|
+
if (isNumber(def.offset)) {
|
|
67
71
|
parts.push(`offset=${def.offset}`);
|
|
68
72
|
}
|
|
69
73
|
const include = Array.isArray(def.include) ? def.include.join(",") : def.include;
|
|
@@ -82,23 +86,22 @@ export function buildDataUrl(
|
|
|
82
86
|
* Resolve an id value: `#/$params/<name>` refs read route params, everything else passes through as
|
|
83
87
|
* a string (template placeholders included).
|
|
84
88
|
*
|
|
85
|
-
* @param {
|
|
89
|
+
* @param {JsonValue | undefined} id - The def's id value
|
|
86
90
|
* @param {Record<string, string> | undefined} pathParams - Route params when known
|
|
87
91
|
* @returns {string | undefined}
|
|
88
92
|
*/
|
|
89
93
|
export function resolveIdValue(
|
|
90
|
-
id:
|
|
94
|
+
id: JsonValue | undefined,
|
|
91
95
|
pathParams: Record<string, string> | undefined,
|
|
92
96
|
): string | undefined {
|
|
93
97
|
if (id === null || id === undefined) {
|
|
94
98
|
return undefined;
|
|
95
99
|
}
|
|
96
|
-
if (
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
100
|
+
if (isRef(id) && id.$ref.startsWith("#/$params/")) {
|
|
101
|
+
const param = id.$ref.slice("#/$params/".length);
|
|
102
|
+
return pathParams?.[param];
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(id) || isJsonObject(id)) {
|
|
102
105
|
return undefined;
|
|
103
106
|
}
|
|
104
107
|
return String(id);
|
|
@@ -108,19 +111,20 @@ export function resolveIdValue(
|
|
|
108
111
|
* Emit the JS source for a literal value inside a lowered Function body. Strings carrying `${...}`
|
|
109
112
|
* become template literals (evaluated against state at runtime); everything else is inlined JSON.
|
|
110
113
|
*
|
|
111
|
-
* @param {
|
|
114
|
+
* @param {JsonValue | undefined} value
|
|
112
115
|
* @returns {string}
|
|
113
116
|
*/
|
|
114
|
-
export function jsValueSource(value:
|
|
115
|
-
if (
|
|
117
|
+
export function jsValueSource(value: JsonValue | undefined): string {
|
|
118
|
+
if (isString(value) && value.includes("${")) {
|
|
116
119
|
const escaped = value.replaceAll("\\", String.raw`\\`).replaceAll("`", "\\`");
|
|
117
120
|
return `\`${escaped}\``;
|
|
118
121
|
}
|
|
119
|
-
|
|
122
|
+
// JSON.stringify(undefined) yields undefined rather than a string; an absent value inlines null.
|
|
123
|
+
return value === undefined ? "null" : JSON.stringify(value);
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/** Emit the JS source of an object literal whose values go through {@link jsValueSource}. */
|
|
123
|
-
export function jsObjectSource(values:
|
|
127
|
+
export function jsObjectSource(values: JsonObject | undefined): string {
|
|
124
128
|
if (!values || Object.keys(values).length === 0) {
|
|
125
129
|
return "{}";
|
|
126
130
|
}
|
package/src/table-state.ts
CHANGED
|
@@ -14,42 +14,55 @@
|
|
|
14
14
|
* cache-busting param — see table-shared.ts.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
17
18
|
import { buildDataUrl, resolveIdValue } from "./table-shared.ts";
|
|
19
|
+
import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
|
|
20
|
+
import type { DynamicRow } from "./query.ts";
|
|
18
21
|
import type { TableQueryDef } from "./table-shared.ts";
|
|
22
|
+
import type { TableNodeConfig, getEntry, queryTable } from "./table-node.ts";
|
|
19
23
|
|
|
20
24
|
/** Route/document context injected by hosts under `_document`. */
|
|
21
25
|
interface DocumentContext {
|
|
22
26
|
route?: { _pathParams?: Record<string, string> };
|
|
23
|
-
[key: string]: unknown;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/** The context object hosts pass to `lower(def, context)`. */
|
|
27
30
|
export interface LowerContext {
|
|
28
31
|
route?: { _pathParams?: Record<string, string>; sourcePath?: string };
|
|
29
|
-
projectConfig?:
|
|
32
|
+
projectConfig?: JsonObject;
|
|
30
33
|
root?: string;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
interface TableStateConfig extends
|
|
34
|
-
id?: unknown;
|
|
36
|
+
interface TableStateConfig extends TableNodeConfig {
|
|
35
37
|
_document?: DocumentContext;
|
|
36
|
-
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A TableQuery/TableEntry def as authored in a document, which is pure JSON: the query options, the
|
|
42
|
+
* `id` and `default` keys `lower` reads, and the class keys it carries past ($prototype, …).
|
|
43
|
+
*/
|
|
44
|
+
interface TableLowerDef extends TableQueryDef {
|
|
45
|
+
id?: JsonValue;
|
|
46
|
+
default?: JsonValue;
|
|
47
|
+
[key: string]: JsonValue | undefined;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/** True when running in a browser (canvas/client); node hosts take the direct-database path. */
|
|
40
51
|
function inBrowser(): boolean {
|
|
41
|
-
return typeof document !== "undefined" &&
|
|
52
|
+
return typeof document !== "undefined" && isFunction(globalThis.fetch);
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
/** The surface of the node-only sibling module (see table-node.ts). */
|
|
45
56
|
interface TableNodeModule {
|
|
46
|
-
queryTable:
|
|
47
|
-
getEntry:
|
|
57
|
+
queryTable: typeof queryTable;
|
|
58
|
+
getEntry: typeof getEntry;
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
/** Import the node-only sibling through an opaque specifier (bundlers must not follow it). */
|
|
51
62
|
async function nodeModule(): Promise<TableNodeModule> {
|
|
52
63
|
const specifier = "./table-node.ts";
|
|
64
|
+
/* SAFETY: the opaque specifier is the literal "./table-node.ts" sibling, whose exports include
|
|
65
|
+
these two functions with exactly the types imported above. */
|
|
53
66
|
return (await import(specifier)) as TableNodeModule;
|
|
54
67
|
}
|
|
55
68
|
|
|
@@ -61,10 +74,12 @@ export class TableQuery {
|
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
/** Rows matching the def's filter/sort/limit — over HTTP in browsers, directly under node. */
|
|
64
|
-
async resolve(): Promise<
|
|
77
|
+
async resolve(): Promise<DynamicRow[]> {
|
|
65
78
|
if (inBrowser()) {
|
|
66
79
|
const response = await fetch(buildDataUrl(this.config));
|
|
67
|
-
|
|
80
|
+
/* SAFETY: a 2xx from GET /_jx/data/:table is the data mount's list answer, Response.json()
|
|
81
|
+
of the row array (worker.ts listRows), so the parsed body is an array of row objects. */
|
|
82
|
+
return response.ok ? ((await response.json()) as DynamicRow[]) : [];
|
|
68
83
|
}
|
|
69
84
|
try {
|
|
70
85
|
const { queryTable } = await nodeModule();
|
|
@@ -77,11 +92,11 @@ export class TableQuery {
|
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
/** Static `lower` capability: compile away into a core Request def (client fetch). */
|
|
80
|
-
static lower(def:
|
|
95
|
+
static lower(def: TableLowerDef): JsonObject {
|
|
81
96
|
return {
|
|
82
97
|
$prototype: "Request",
|
|
83
98
|
timing: "client",
|
|
84
|
-
url: buildDataUrl(def
|
|
99
|
+
url: buildDataUrl(def, { versionParam: true }),
|
|
85
100
|
...(def.default === undefined ? { default: [] } : { default: def.default }),
|
|
86
101
|
};
|
|
87
102
|
}
|
|
@@ -95,7 +110,7 @@ export class TableEntry {
|
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
/** One row by id (`#/$params/` refs resolve against the route), or null. */
|
|
98
|
-
async resolve(): Promise<
|
|
113
|
+
async resolve(): Promise<DynamicRow | null> {
|
|
99
114
|
const { _document } = this.config;
|
|
100
115
|
const id = resolveIdValue(this.config.id, _document?.route?._pathParams);
|
|
101
116
|
if (id === undefined || id === "") {
|
|
@@ -103,7 +118,9 @@ export class TableEntry {
|
|
|
103
118
|
}
|
|
104
119
|
if (inBrowser()) {
|
|
105
120
|
const response = await fetch(buildDataUrl(this.config, { id }));
|
|
106
|
-
|
|
121
|
+
/* SAFETY: a 2xx from GET /_jx/data/:table/:id is the data mount's single-row answer,
|
|
122
|
+
Response.json() of one row object (worker.ts getRow). */
|
|
123
|
+
return response.ok ? ((await response.json()) as DynamicRow) : null;
|
|
107
124
|
}
|
|
108
125
|
try {
|
|
109
126
|
const { getEntry } = await nodeModule();
|
|
@@ -116,7 +133,7 @@ export class TableEntry {
|
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
/** Static `lower` capability: compile away into a core Request def for one row. */
|
|
119
|
-
static lower(def:
|
|
136
|
+
static lower(def: TableLowerDef, context: LowerContext = {}): JsonObject {
|
|
120
137
|
const id = resolveIdValue(def.id, context.route?._pathParams);
|
|
121
138
|
if (id === undefined) {
|
|
122
139
|
console.warn(
|
|
@@ -127,7 +144,7 @@ export class TableEntry {
|
|
|
127
144
|
return {
|
|
128
145
|
$prototype: "Request",
|
|
129
146
|
timing: "client",
|
|
130
|
-
url: buildDataUrl(def
|
|
147
|
+
url: buildDataUrl(def, { id: id ?? "", versionParam: true }),
|
|
131
148
|
...(def.default === undefined ? { default: null } : { default: def.default }),
|
|
132
149
|
};
|
|
133
150
|
}
|
package/src/table-update.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { lowerActionDef, makeActionHandler } from "./table-actions.ts";
|
|
10
|
+
import type { JsonObject } from "@jxsuite/schema/types";
|
|
10
11
|
import type { ActionDef } from "./table-actions.ts";
|
|
11
12
|
import type { LowerContext } from "./table-state.ts";
|
|
12
13
|
|
|
@@ -23,7 +24,7 @@ export class TableUpdate {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** Static `lower` capability: compile away into an inline Function def. */
|
|
26
|
-
static lower(def: ActionDef, context: LowerContext = {}):
|
|
27
|
+
static lower(def: ActionDef, context: LowerContext = {}): JsonObject {
|
|
27
28
|
return lowerActionDef("PATCH", def, context);
|
|
28
29
|
}
|
|
29
30
|
}
|