@jxsuite/connector 0.6.2 → 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/src/types.ts CHANGED
@@ -7,6 +7,9 @@
7
7
  * connection definitions declared in project.json's `data` and `connections` sections.
8
8
  */
9
9
 
10
+ import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
11
+ import type { HostEnv } from "./provider-utils.ts";
12
+
10
13
  // ─── Field / table schema ─────────────────────────────────────────────────────
11
14
 
12
15
  /**
@@ -22,9 +25,9 @@ export interface ColumnFieldSchema {
22
25
  items?: ColumnFieldSchema;
23
26
  properties?: Record<string, ColumnFieldSchema>;
24
27
  required?: string[];
25
- enum?: unknown[];
28
+ enum?: JsonValue[];
26
29
  format?: string;
27
- default?: unknown;
30
+ default?: JsonValue;
28
31
  description?: string;
29
32
  }
30
33
 
@@ -57,7 +60,8 @@ export interface ConnectionDef {
57
60
  file?: string;
58
61
  /** Connection name, injected by hosts when passing the def to connector capabilities. */
59
62
  $name?: string;
60
- [key: string]: unknown;
63
+ /** Provider-specific identifiers; the entry is project.json data, so every value is JSON. */
64
+ [key: string]: JsonValue | undefined;
61
65
  }
62
66
 
63
67
  // ─── Permissions ──────────────────────────────────────────────────────────────
@@ -79,13 +83,24 @@ export const DEFAULT_PERMISSIONS: Record<PermissionAction, PermissionRule> = {
79
83
 
80
84
  // ─── Auth contract (implemented by @jxsuite/auth in Part 4b) ─────────────────
81
85
 
86
+ /**
87
+ * One field of an auth provider's user or session record: JSON values, plus the `Date` fields
88
+ * (`createdAt`, `expiresAt`, …) a server-side adapter returns; the client copy carries ISO
89
+ * strings.
90
+ */
91
+ export type AuthRecordValue = JsonValue | Date | undefined;
92
+
93
+ /** A user or session record as the auth provider returns it. */
94
+ export type AuthRecord = Record<string, AuthRecordValue>;
95
+
82
96
  /** The session shape auth hooks resolve from a request. */
83
97
  export interface SessionInfo {
84
98
  userId: string;
85
99
  role?: string;
86
100
  /** Full user record, when the auth provider exposes one. */
87
- user?: Record<string, unknown>;
88
- [key: string]: unknown;
101
+ user?: AuthRecord;
102
+ /** Provider extras (the session record itself, for Better Auth). */
103
+ [key: string]: AuthRecordValue | AuthRecord;
89
104
  }
90
105
 
91
106
  export interface AuthorizeInput {
@@ -102,38 +117,30 @@ export interface AuthorizeDecision {
102
117
  status?: number;
103
118
  error?: string;
104
119
  /** Columns forced onto insert/update payloads (e.g. the owner id). */
105
- setColumns?: Record<string, unknown>;
120
+ setColumns?: JsonObject;
106
121
  /** Row filter scoping reads/writes to the owner: `WHERE <field> = <value>`. */
107
- whereOwner?: { field: string; value: unknown } | null;
122
+ whereOwner?: { field: string; value: JsonValue } | null;
108
123
  }
109
124
 
110
125
  /** The hooks an auth mount publishes on the shared server context. */
111
126
  export interface JxAuthHooks {
112
- getSession: (request: Request, env: Record<string, unknown>) => Promise<SessionInfo | null>;
113
- authorize: (input: AuthorizeInput, env: Record<string, unknown>) => Promise<AuthorizeDecision>;
127
+ getSession: (request: Request, env: HostEnv) => Promise<SessionInfo | null>;
128
+ authorize: (input: AuthorizeInput, env: HostEnv) => Promise<AuthorizeDecision>;
114
129
  }
115
130
 
116
131
  /**
117
132
  * One shared mutable context per worker isolate, passed to every mount in `server.order`
118
133
  * (specs/extensions.md §11). The auth mount (order 10) sets `auth`; the data mount (order 20)
119
- * consumes it and fails closed when it is absent.
134
+ * consumes it and fails closed when it is absent. A mount that publishes another service declares
135
+ * its member by augmenting this interface (`declare module "@jxsuite/connector/types"`).
120
136
  */
121
137
  export interface JxServerContext {
122
138
  auth?: JxAuthHooks;
123
- [key: string]: unknown;
124
139
  }
125
140
 
126
141
  // ─── Deploy / dialect capability results ─────────────────────────────────────
127
142
 
128
- /** Result shape of the `deploySchema` connector capability. */
129
- export interface DeploySchemaResult {
130
- statements: string[];
131
- applied: boolean;
132
- warnings: string[];
133
- }
134
-
135
- /** Result shape of the `testConnection` connector capability. */
136
- export interface TestConnectionResult {
137
- ok: boolean;
138
- error?: string;
139
- }
143
+ /* The `deploySchema` and `testConnection` result shapes are declared once in @jxsuite/schema beside
144
+ the capability names, because the hosts that call these capabilities cannot depend on this
145
+ extension (specs/extensions.md §2). Re-exported so this package's dependents keep one import. */
146
+ export type { DeploySchemaResult, TestConnectionResult } from "@jxsuite/schema/format-registry";
package/src/validate.ts CHANGED
@@ -9,7 +9,9 @@
9
9
  * validate cleanly.
10
10
  */
11
11
 
12
+ import { isBoolean, isJsonObject, isNumber, isString } from "@jxsuite/schema/guards";
12
13
  import { parseRefPointer } from "./columns.ts";
14
+ import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
13
15
  import type { ColumnFieldSchema, TableDef } from "./types.ts";
14
16
 
15
17
  export interface ValidationIssue {
@@ -21,7 +23,7 @@ export interface ValidationResult {
21
23
  valid: boolean;
22
24
  errors: ValidationIssue[];
23
25
  /** The coerced payload (schema-typed values), keyed by schema field name. */
24
- value: Record<string, unknown>;
26
+ value: JsonObject;
25
27
  }
26
28
 
27
29
  /** Columns the data mount owns; clients may never write them. */
@@ -31,26 +33,25 @@ const READ_ONLY_FIELDS = new Set(["id", "created_at", "updated_at"]);
31
33
  * Validate (and coerce) a row payload against a table definition.
32
34
  *
33
35
  * @param {TableDef} table - The table definition from the `data` section
34
- * @param {unknown} payload - Parsed request body
36
+ * @param {JsonValue} payload - Parsed request body
35
37
  * @param {{ partial?: boolean }} [options] - `partial` skips required-field checks (PATCH)
36
38
  * @returns {ValidationResult}
37
39
  */
38
40
  export function validateRow(
39
41
  table: TableDef,
40
- payload: unknown,
42
+ payload: JsonValue,
41
43
  options: { partial?: boolean } = {},
42
44
  ): ValidationResult {
43
45
  const { partial = false } = options;
44
46
  const errors: ValidationIssue[] = [];
45
- const value: Record<string, unknown> = {};
47
+ const value: JsonObject = {};
46
48
 
47
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
49
+ if (!isJsonObject(payload)) {
48
50
  return { errors: [{ field: "", message: "Body must be a JSON object" }], valid: false, value };
49
51
  }
50
- const body = payload as Record<string, unknown>;
51
52
  const fields = table.schema.properties ?? {};
52
53
 
53
- for (const [key, raw] of Object.entries(body)) {
54
+ for (const [key, raw] of Object.entries(payload)) {
54
55
  if (READ_ONLY_FIELDS.has(key)) {
55
56
  errors.push({ field: key, message: "Read-only field" });
56
57
  continue;
@@ -68,7 +69,7 @@ export function validateRow(
68
69
 
69
70
  if (!partial) {
70
71
  for (const required of table.schema.required ?? []) {
71
- if (body[required] === undefined) {
72
+ if (payload[required] === undefined) {
72
73
  errors.push({ field: required, message: "Required field is missing" });
73
74
  }
74
75
  }
@@ -85,23 +86,23 @@ const INVALID = Symbol("invalid");
85
86
  *
86
87
  * @param {string} field
87
88
  * @param {ColumnFieldSchema} schema
88
- * @param {unknown} raw
89
+ * @param {JsonValue} raw
89
90
  * @param {ValidationIssue[]} errors
90
- * @returns {unknown} The coerced value, or the INVALID sentinel
91
+ * @returns {JsonValue | typeof INVALID} The coerced value, or the INVALID sentinel
91
92
  */
92
93
  function coerceField(
93
94
  field: string,
94
95
  schema: ColumnFieldSchema,
95
- raw: unknown,
96
+ raw: JsonValue,
96
97
  errors: ValidationIssue[],
97
- ): unknown {
98
- if (raw === null || raw === undefined) {
98
+ ): JsonValue | typeof INVALID {
99
+ if (raw === null) {
99
100
  return null;
100
101
  }
101
102
 
102
103
  const toOne = parseRefPointer(schema.$ref);
103
104
  if (toOne) {
104
- if (typeof raw !== "string" || raw === "") {
105
+ if (!isString(raw) || raw === "") {
105
106
  errors.push({ field, message: "Reference value must be an entry id string" });
106
107
  return INVALID;
107
108
  }
@@ -111,7 +112,7 @@ function coerceField(
111
112
  const itemsRef = schema.type === "array" ? parseRefPointer(schema.items?.$ref) : null;
112
113
  if (itemsRef) {
113
114
  const list = parseMaybeJson(raw);
114
- if (!Array.isArray(list) || list.some((item) => typeof item !== "string" || item === "")) {
115
+ if (!Array.isArray(list) || list.some((item) => !isString(item) || item === "")) {
115
116
  errors.push({ field, message: "Reference list must be an array of entry id strings" });
116
117
  return INVALID;
117
118
  }
@@ -131,14 +132,14 @@ function coerceField(
131
132
  }
132
133
 
133
134
  /** Coerce a scalar payload value to its schema type, or INVALID. */
134
- function coerceScalar(schema: ColumnFieldSchema, raw: unknown): unknown {
135
+ function coerceScalar(schema: ColumnFieldSchema, raw: JsonValue): JsonValue | typeof INVALID {
135
136
  switch (schema.type) {
136
137
  case "number":
137
138
  case "integer": {
138
139
  let num: number;
139
- if (typeof raw === "number") {
140
+ if (isNumber(raw)) {
140
141
  num = raw;
141
- } else if (typeof raw === "string" && raw.trim() !== "" && !Number.isNaN(Number(raw))) {
142
+ } else if (isString(raw) && raw.trim() !== "" && !Number.isNaN(Number(raw))) {
142
143
  num = Number(raw);
143
144
  } else {
144
145
  return INVALID;
@@ -149,7 +150,7 @@ function coerceScalar(schema: ColumnFieldSchema, raw: unknown): unknown {
149
150
  return num;
150
151
  }
151
152
  case "boolean": {
152
- if (typeof raw === "boolean") {
153
+ if (isBoolean(raw)) {
153
154
  return raw;
154
155
  }
155
156
  if (raw === "true" || raw === "on" || raw === "1" || raw === 1) {
@@ -166,19 +167,17 @@ function coerceScalar(schema: ColumnFieldSchema, raw: unknown): unknown {
166
167
  }
167
168
  case "object": {
168
169
  const parsed = parseMaybeJson(raw);
169
- return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
170
- ? parsed
171
- : INVALID;
170
+ return isJsonObject(parsed) ? parsed : INVALID;
172
171
  }
173
172
  default: {
174
- return typeof raw === "string" ? raw : INVALID;
173
+ return isString(raw) ? raw : INVALID;
175
174
  }
176
175
  }
177
176
  }
178
177
 
179
178
  /** Parse JSON-looking strings (FormData carries arrays/objects as text); pass everything else. */
180
- function parseMaybeJson(raw: unknown): unknown {
181
- if (typeof raw !== "string") {
179
+ function parseMaybeJson(raw: JsonValue): JsonValue {
180
+ if (!isString(raw)) {
182
181
  return raw;
183
182
  }
184
183
  const text = raw.trim();
@@ -186,7 +185,7 @@ function parseMaybeJson(raw: unknown): unknown {
186
185
  return raw;
187
186
  }
188
187
  try {
189
- return JSON.parse(text) as unknown;
188
+ return JSON.parse(text);
190
189
  } catch {
191
190
  return raw;
192
191
  }