@schemic/core 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/lib/authoring.d.ts +89 -0
- package/lib/authoring.js +187 -0
- package/lib/authoring.js.map +1 -0
- package/lib/chunk-C4D6JWSE.js +54 -0
- package/lib/chunk-C4D6JWSE.js.map +1 -0
- package/lib/chunk-T23RNU7G.js +304 -0
- package/lib/chunk-T23RNU7G.js.map +1 -0
- package/lib/config-TIiKDd9t.d.ts +97 -0
- package/lib/config.d.ts +1 -0
- package/lib/config.js +8 -0
- package/lib/config.js.map +1 -0
- package/lib/driver-Dh5hLKHm.d.ts +736 -0
- package/lib/driver.d.ts +150 -0
- package/lib/driver.js +47 -0
- package/lib/driver.js.map +1 -0
- package/lib/index.d.ts +84 -0
- package/lib/index.js +794 -0
- package/lib/index.js.map +1 -0
- package/lib/testing.d.ts +29 -0
- package/lib/testing.js +111 -0
- package/lib/testing.js.map +1 -0
- package/package.json +93 -0
- package/src/authoring.ts +304 -0
- package/src/cli-kit/config.ts +179 -0
- package/src/cli-kit/diff.ts +230 -0
- package/src/cli-kit/filter.ts +159 -0
- package/src/cli-kit/merge.ts +380 -0
- package/src/cli-kit/meta.ts +123 -0
- package/src/cli-kit/pager.ts +42 -0
- package/src/cli-kit/schema.ts +186 -0
- package/src/cli-kit/style.ts +24 -0
- package/src/config.ts +51 -0
- package/src/connection.ts +78 -0
- package/src/driver/driver.ts +300 -0
- package/src/driver/index.ts +31 -0
- package/src/driver/portable-ir.ts +51 -0
- package/src/driver/portable.ts +124 -0
- package/src/driver/sdk.ts +66 -0
- package/src/index.ts +145 -0
- package/src/kind/index.ts +28 -0
- package/src/kind/plan.ts +390 -0
- package/src/kind/registry.ts +225 -0
- package/src/testing.ts +181 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vertio Solutions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# @schemic/core
|
|
2
|
+
|
|
3
|
+
Author [SurrealDB](https://surrealdb.com) schemas with [Zod](https://zod.dev).
|
|
4
|
+
|
|
5
|
+
- **`s.*`** — a drop-in for `z.*` that also carries SurrealQL metadata.
|
|
6
|
+
- **`defineTable` / `defineField`** — generate `DEFINE TABLE` / `DEFINE FIELD` DDL from your schema.
|
|
7
|
+
- **`decode` / `encode`** — map DB rows ⇄ app objects across Zod's two channels via codecs
|
|
8
|
+
(`DateTime`⇄`Date`, `Uuid`⇄`string`, `RecordId`, …).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bun add @schemic/core surrealdb zod
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`surrealdb` and `zod` are peer dependencies.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { s, table, relation, defineTable, type App } from "@schemic/core";
|
|
22
|
+
import { surql } from "surrealdb";
|
|
23
|
+
|
|
24
|
+
export const User = table("user", {
|
|
25
|
+
id: s.string(), // -> record<user>
|
|
26
|
+
name: s.string(),
|
|
27
|
+
email: s.email(),
|
|
28
|
+
status: s.string().$default("pending"), // DB-side DEFAULT
|
|
29
|
+
createdAt: s.datetime().$default(surql`time::now()`).$readonly(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const Friend = relation("friend", {
|
|
33
|
+
strength: s.number().$gte(0).$lte(1), // -> ASSERT $value >= 0 AND $value <= 1
|
|
34
|
+
})
|
|
35
|
+
.from(User)
|
|
36
|
+
.to(User);
|
|
37
|
+
|
|
38
|
+
// SurrealQL DDL:
|
|
39
|
+
console.log(defineTable(User));
|
|
40
|
+
// DEFINE TABLE user TYPE NORMAL SCHEMAFULL;
|
|
41
|
+
// DEFINE FIELD name ON TABLE user TYPE string;
|
|
42
|
+
// ...
|
|
43
|
+
|
|
44
|
+
// Build a CREATE payload (DB-filled fields optional), then decode a row back:
|
|
45
|
+
const payload = User.encode({ name: "Alice", email: "alice@example.com" });
|
|
46
|
+
type AppUser = App<typeof User>; // id: RecordId, createdAt: Date, ...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Reading & writing
|
|
50
|
+
|
|
51
|
+
The whole JS ⇄ DB mapping rides on Zod's two codec channels:
|
|
52
|
+
|
|
53
|
+
- **`decode`** (read, wire → app) turns a DB row into your app object (`DateTime` → `Date`,
|
|
54
|
+
`Uuid` → `string`, `RecordId`, …).
|
|
55
|
+
- **`encode` / `encodePartial`** (write, app → wire) build a payload. They're **create/patch-
|
|
56
|
+
shaped**: DB-filled fields (`$default`, `id`) are *optional* (the DB fills them), absent keys
|
|
57
|
+
are omitted, and each provided value is validated. Use `encode` with `CONTENT` (create) and
|
|
58
|
+
`encodePartial` with `MERGE` (patch).
|
|
59
|
+
- `parse*` are kept as **`@deprecated`** aliases of `decode*` (for `z`-API familiarity).
|
|
60
|
+
|
|
61
|
+
`encode` / `encodePartial` return a typed `Partial<Wire<T>>` (codec fields are wire-typed —
|
|
62
|
+
`createdAt` is a `DateTime`, not a `Date`). They **throw** a `ZodError` on invalid input. For
|
|
63
|
+
the non-throwing form use `safeEncode` / `safeEncodePartial`, which return a Zod-style
|
|
64
|
+
`{ success: true; data } | { success: false; error }` (all field errors aggregated into one
|
|
65
|
+
`ZodError`). Each has an async twin (`encodeAsync` / `safeEncodeAsync` / …) for async
|
|
66
|
+
refinements. `TableDef.system.{encode,safeEncode,…}` are the same over the full shape,
|
|
67
|
+
including `$internal()` fields. If you ever need the **raw full-object codec** (no create-
|
|
68
|
+
shaping), that's just `z.encode(table.object, app)`.
|
|
69
|
+
|
|
70
|
+
Field-level codecs work the same way directly on a field:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
s.datetime().decode(dbDateTime); // -> Date
|
|
74
|
+
s.datetime().encode(new Date()); // -> DateTime
|
|
75
|
+
s.uuid().encode("0190b6e0-…"); // -> Uuid
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
See [`examples/`](./examples) for a full schema, a live demo (`bun examples/demo.ts`),
|
|
79
|
+
and a small CRUD server.
|
|
80
|
+
|
|
81
|
+
## Nested objects
|
|
82
|
+
|
|
83
|
+
`s.object({ ... })` builds a nested SurrealQL `object`, and @schemic/core looks *through* it so
|
|
84
|
+
each nested field keeps its own DDL metadata and create-optionality:
|
|
85
|
+
|
|
86
|
+
- A nested field with a DB `$default` (or `$value(..., { optional: true })`) is
|
|
87
|
+
**create-optional** in `encode` — omit it and the DB fills it — exactly like a top-level
|
|
88
|
+
defaulted field. So you can pass a *partial* nested object and let the DB complete it:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const Project = table("project", {
|
|
92
|
+
name: s.string(),
|
|
93
|
+
settings: s.object({
|
|
94
|
+
isPublic: s.boolean().$default(surql`false`),
|
|
95
|
+
defaultView: s.enum(["list", "board"]).$default("list"),
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
Project.encode({ name: "Launch", settings: { defaultView: "board" } });
|
|
100
|
+
// -> { name, settings: { defaultView: "board" } } (the DB fills settings.isPublic)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- On the **decoded** side those nested fields stay **required** in `App<T>` — a stored row has
|
|
104
|
+
them — consistent with how top-level defaulted fields behave.
|
|
105
|
+
|
|
106
|
+
### `encode` (CONTENT) vs `encodePartial` (MERGE)
|
|
107
|
+
|
|
108
|
+
`encode` builds a **`CONTENT`** payload; `encodePartial` builds a **deep-partial `MERGE`**
|
|
109
|
+
payload — every nested key is optional, mirroring SurrealDB's `MERGE`, which **recursively
|
|
110
|
+
deep-merges** (siblings are preserved at every level):
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
Project.encodePartial({ settings: { defaultView: "board" } });
|
|
114
|
+
// -> { settings: { defaultView: "board" } } -> UPDATE $id MERGE $payload
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The library only **builds the payload** — you choose the statement. Pair `encode` with `CONTENT`
|
|
118
|
+
and `encodePartial` with `MERGE`. **Warning:** sending a *partial* payload with `CONTENT`
|
|
119
|
+
**replaces** the record (unsupplied fields are dropped); use `MERGE` for partial writes.
|
|
120
|
+
|
|
121
|
+
### Atomic (object-level) validation
|
|
122
|
+
|
|
123
|
+
Only objects built with `s.object` are flattened/recursed. A field that holds a **raw, refined
|
|
124
|
+
`z.object`** is validated **atomically** — provide it whole, and its object-level `refine` runs:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { z } from "zod";
|
|
128
|
+
|
|
129
|
+
table("booking", {
|
|
130
|
+
// validated all-or-nothing; the refine runs on the whole object
|
|
131
|
+
range: z.object({ from: z.number(), to: z.number() }).refine((r) => r.from <= r.to),
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
That's the built-in escape when you want all-or-nothing / object-level checks. For full manual
|
|
136
|
+
control, build the payload yourself and pass it via `surql` — `encode`/`encodePartial` are just
|
|
137
|
+
conveniences, not a requirement.
|
|
138
|
+
|
|
139
|
+
## Permissions
|
|
140
|
+
|
|
141
|
+
Author row-level `PERMISSIONS` with `TableDef.permissions(spec)` and `SField.$permissions(spec)`.
|
|
142
|
+
A `spec` is `true` (FULL) / `false` (NONE) / a `surql` `WHERE` expr (shared by every op) / a
|
|
143
|
+
per-op object. In a per-op object each op is `true`/`false`/a `surql` expr, or `` `same as <op>` ``
|
|
144
|
+
to reuse another op's rule; ops with an identical resolved rule auto-merge into one `FOR a, b …`
|
|
145
|
+
clause. Table permissions cover `select`/`create`/`update`/`delete`; **fields have no `delete`** op.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
table("project", {
|
|
149
|
+
owner: User.record().$default(surql`$auth.id`).$readonly(),
|
|
150
|
+
// ...
|
|
151
|
+
}).permissions({
|
|
152
|
+
select: surql`owner = $auth.id OR settings.isPublic = true`,
|
|
153
|
+
create: surql`owner = $auth.id`,
|
|
154
|
+
update: "same as create",
|
|
155
|
+
delete: "same as create",
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Table permissions fold into the single generated `DEFINE TABLE` (no separate `OVERWRITE`).
|
|
160
|
+
**Omitted-op asymmetry** (it mirrors SurrealDB's own defaults): an omitted op defaults to
|
|
161
|
+
**NONE** (deny) on a *table* but to **FULL** on a *field* — the table is the gate, so to lock a
|
|
162
|
+
field op you must set it `false` explicitly.
|
|
163
|
+
|
|
164
|
+
## Asserts / constraints
|
|
165
|
+
|
|
166
|
+
A field can accumulate several `ASSERT` fragments that AND-combine into one `ASSERT`
|
|
167
|
+
clause (deduped, order preserved). There are three sources:
|
|
168
|
+
|
|
169
|
+
- **Format builders bake by default.** `s.email()` → `ASSERT string::is_email($value)`,
|
|
170
|
+
`s.url()` → `string::is_url`, and likewise `ulid` / `ipv4` / `ipv6` — i.e. every
|
|
171
|
+
builder whose `string::is_*` validator exists on the server. SurrealDB **3.x** uses the
|
|
172
|
+
underscore form (`string::is_email`, **not** `string::is::email`). Formats with no
|
|
173
|
+
server validator (`nanoid`, `cuid`/`cuid2`, `xid`, `ksuid`, `cidrv4`/`cidrv6`, `guid`,
|
|
174
|
+
`base64`/`base64url`, `e164`, `jwt`, `emoji`) stay assert-free — no fabricated regex.
|
|
175
|
+
`s.uuid()` is the native `uuid` type (no assert).
|
|
176
|
+
- **`$`-constraints** apply the matching Zod check app-side **and** push a type-aware DB
|
|
177
|
+
fragment (string vs. number is read from the schema):
|
|
178
|
+
- `.$min(n)` / `.$max(n)` — string: `string::len($value) >= n` / `<= n`; number: `$value >= n` / `<= n`
|
|
179
|
+
- `.$length(n)` — string: `string::len($value) == n`
|
|
180
|
+
- `.$regex(/re/)` — string: `$value = /re/`
|
|
181
|
+
- `.$gt(n)` / `.$gte(n)` / `.$lt(n)` / `.$lte(n)` — number: `$value > / >= / < / <= n`
|
|
182
|
+
- **`.$assert(...)`** — `.$assert(surql\`…\`)` pushes a custom fragment; `.$assert()` (no
|
|
183
|
+
args) derives fragments from the field's existing Zod checks (formats, length, regex,
|
|
184
|
+
number bounds), best-effort.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
s.string().$min(1).$max(120); // string::len($value) >= 1 AND ... <= 120
|
|
188
|
+
s.number().$gte(0).$lte(1); // $value >= 0 AND $value <= 1
|
|
189
|
+
s.email().$assert(surql`$value != $forbidden`); // string::is_email($value) AND $value != $forbidden
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Live queries
|
|
193
|
+
|
|
194
|
+
There's no special live API — a subscription payload is just a row, so decode it exactly like
|
|
195
|
+
a query result:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
await db.live("user", (action, value) => {
|
|
199
|
+
if (action === "CLOSE") return;
|
|
200
|
+
const user = User.decode(value); // decoded App<User> — RecordId, Date, …
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
A typed query + live layer (results decoded automatically) is planned in `@schemic/core/orm`.
|
|
205
|
+
|
|
206
|
+
## Develop
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
bun test # unit + live (live skips when no SurrealDB is reachable)
|
|
210
|
+
bun run typecheck
|
|
211
|
+
bun run build # -> lib/ (ESM + d.ts) via tsup
|
|
212
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Any field of ANY dialect — the base type the helpers + wrappers accept. */
|
|
4
|
+
type AnyField = SFieldBase<z.ZodType, string, any>;
|
|
5
|
+
/** The Zod schema a field (or a raw Zod schema) carries. */
|
|
6
|
+
type SchemaOf<F> = F extends SFieldBase<infer S, string, any> ? S : F extends z.ZodType ? F : never;
|
|
7
|
+
/** The `Flags` channel a field carries (driver `$`-methods brand it; widens to `string` for `Shape`). */
|
|
8
|
+
type FlagsOf<F> = F extends SFieldBase<z.ZodType, infer Fl, any> ? Fl : never;
|
|
9
|
+
/** The schema one wrapper down — what `unwrap()` returns. */
|
|
10
|
+
type InnerOf<S extends z.ZodType> = S extends z.ZodOptional<infer I extends z.ZodType> ? I : S extends z.ZodNullable<infer I extends z.ZodType> ? I : S extends z.ZodDefault<infer I extends z.ZodType> ? I : S extends z.ZodPrefault<infer I extends z.ZodType> ? I : S extends z.ZodCatch<infer I extends z.ZodType> ? I : S extends z.ZodReadonly<infer I extends z.ZodType> ? I : S extends z.ZodArray<infer I extends z.ZodType> ? I : S;
|
|
11
|
+
/**
|
|
12
|
+
* Maps an object schema (built via a driver's `s.object`) to its original field shape, so nested
|
|
13
|
+
* fields keep their authoring metadata through generation. Kept on the schema, not the field, so it
|
|
14
|
+
* composes through `array()`/`optional()`/nesting.
|
|
15
|
+
*/
|
|
16
|
+
declare const objectFieldsRegistry: WeakMap<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, Record<string, AnyField>>;
|
|
17
|
+
/**
|
|
18
|
+
* The PORTABLE, dialect-agnostic field base. Holds the Zod schema, an opaque per-dialect `native`
|
|
19
|
+
* metadata slot, the field-level codecs, and the app-land Zod wrappers (which carry `native` forward
|
|
20
|
+
* via the `rebuild` hook so a chain keeps its concrete dialect type). Each dialect subclasses it to
|
|
21
|
+
* add native authoring (`$`-methods) and re-type the wrappers so a chain stays its own field type.
|
|
22
|
+
*/
|
|
23
|
+
declare abstract class SFieldBase<S extends z.ZodType = z.ZodType, Flags extends string = never, N = unknown> {
|
|
24
|
+
readonly schema: S;
|
|
25
|
+
readonly native: N;
|
|
26
|
+
constructor(schema: S, native: N);
|
|
27
|
+
/** Rebuild a sibling field of the SAME dialect with a new schema/flags. Each dialect overrides it. */
|
|
28
|
+
protected abstract rebuild<S2 extends z.ZodType, F2 extends string>(schema: S2, native: N): SFieldBase<S2, F2, N>;
|
|
29
|
+
/** A fresh, empty native-metadata bag (for wrappers like `or`/`and` that reset it). */
|
|
30
|
+
protected abstract blank(): N;
|
|
31
|
+
/** Decode a DB value to its app type (wire -> app). */
|
|
32
|
+
decode(value: unknown): z.output<S>;
|
|
33
|
+
/** Encode an app value to its DB wire type (app -> wire). */
|
|
34
|
+
encode(value: z.output<S>): z.input<S>;
|
|
35
|
+
decodeAsync(value: unknown): Promise<z.output<S>>;
|
|
36
|
+
encodeAsync(value: z.output<S>): Promise<z.input<S>>;
|
|
37
|
+
safeDecode(value: unknown): z.ZodSafeParseResult<z.core.output<S>>;
|
|
38
|
+
safeEncode(value: z.output<S>): z.ZodSafeParseResult<z.core.input<S>>;
|
|
39
|
+
safeDecodeAsync(value: unknown): Promise<z.ZodSafeParseResult<z.core.output<S>>>;
|
|
40
|
+
safeEncodeAsync(value: z.output<S>): Promise<z.ZodSafeParseResult<z.core.input<S>>>;
|
|
41
|
+
/** @deprecated `parse` decodes a value (wire -> app). Use {@link decode}. */
|
|
42
|
+
parse(value: unknown): z.output<S>;
|
|
43
|
+
/** @deprecated Use {@link safeDecode}. */
|
|
44
|
+
safeParse(value: unknown): z.ZodSafeParseResult<z.core.output<S>>;
|
|
45
|
+
/** @deprecated Use {@link decodeAsync}. */
|
|
46
|
+
parseAsync(value: unknown): Promise<z.output<S>>;
|
|
47
|
+
/** @deprecated Use {@link safeDecodeAsync}. */
|
|
48
|
+
safeParseAsync(value: unknown): Promise<z.ZodSafeParseResult<z.core.output<S>>>;
|
|
49
|
+
optional(): SFieldBase<z.ZodOptional<S>, Flags, N>;
|
|
50
|
+
nullable(): SFieldBase<z.ZodNullable<S>, Flags, N>;
|
|
51
|
+
default(value: z.input<S>): SFieldBase<z.ZodDefault<S>, Flags, N>;
|
|
52
|
+
/** Zod prefault: fill an absent value with `value`, then validate it (unlike `.default`). */
|
|
53
|
+
prefault(value: z.input<S>): SFieldBase<z.ZodPrefault<S>, Flags, N>;
|
|
54
|
+
/** Zod catch: fall back to `value` when parsing fails. */
|
|
55
|
+
catch(value: z.output<S>): SFieldBase<z.ZodCatch<S>, Flags, N>;
|
|
56
|
+
array(): SFieldBase<z.ZodArray<S>, Flags, N>;
|
|
57
|
+
nullish(): SFieldBase<z.ZodOptional<z.ZodNullable<S>>, Flags, N>;
|
|
58
|
+
/** Zod union — `a.or(b)` accepts either. Mirrors Zod's `.or()`. */
|
|
59
|
+
or<F extends AnyField | z.ZodType>(other: F): SFieldBase<z.ZodUnion<[S, SchemaOf<F>]>, never, N>;
|
|
60
|
+
/** Zod intersection — `a.and(b)`. Mirrors Zod's `.and()`. */
|
|
61
|
+
and<F extends AnyField | z.ZodType>(other: F): SFieldBase<z.ZodIntersection<S, SchemaOf<F>>, never, N>;
|
|
62
|
+
refine(check: (arg: z.output<S>) => unknown, params?: string | z.core.$ZodCustomParams): this;
|
|
63
|
+
superRefine(refinement: (arg: z.output<S>, ctx: z.core.$RefinementCtx<z.output<S>>) => void): this;
|
|
64
|
+
check(...checks: (z.core.CheckFn<z.output<S>> | z.core.$ZodCheck<z.output<S>>)[]): this;
|
|
65
|
+
overwrite(fn: (x: z.output<S>) => z.output<S>): this;
|
|
66
|
+
brand<B extends PropertyKey = PropertyKey>(value?: B): this;
|
|
67
|
+
/** Zod's app-side metadata (JSON-schema/docs) — distinct from a driver's `$comment()`. */
|
|
68
|
+
describe(description: string): this;
|
|
69
|
+
meta(data: z.core.GlobalMeta): this;
|
|
70
|
+
/** Zod's app-side readonly (TS-immutable output) — distinct from a driver's `$readonly()`. */
|
|
71
|
+
readonly(): SFieldBase<z.ZodReadonly<S>, Flags, N>;
|
|
72
|
+
/** Zod transform — changes the decoded `App<>` value; the stored (wire) type is unchanged. */
|
|
73
|
+
transform<NewOut>(fn: (arg: z.output<S>, ctx: z.core.$RefinementCtx<z.output<S>>) => NewOut): SFieldBase<z.ZodPipe<S, z.ZodTransform<Awaited<NewOut>, z.output<S>>>, Flags, N>;
|
|
74
|
+
/** Zod pipe — feed this field's output into `target`; the stored (wire) type stays `this`. */
|
|
75
|
+
pipe<T extends z.core.$ZodType<unknown, z.output<S>>>(target: T): SFieldBase<z.ZodPipe<S, T>, Flags, N>;
|
|
76
|
+
/** Peel one wrapper (optional/nullable/default/prefault/catch/readonly/array) off the field. */
|
|
77
|
+
unwrap(): SFieldBase<InnerOf<S>, Flags, N>;
|
|
78
|
+
/** Object-only: allow arbitrary extra keys — `FLEXIBLE` in DDL. Mirrors Zod's `.loose()`. */
|
|
79
|
+
loose(): this;
|
|
80
|
+
/** Object-only: reject unknown keys — the default. Mirrors Zod's `.strict()`. */
|
|
81
|
+
strict(): this;
|
|
82
|
+
/** Alias for {@link loose} — a `FLEXIBLE` object accepting arbitrary keys. */
|
|
83
|
+
flexible(): this;
|
|
84
|
+
private objectMode;
|
|
85
|
+
}
|
|
86
|
+
/** Unwrap a field to its Zod schema (raw Zod schemas pass through). */
|
|
87
|
+
declare const toZod: (v: AnyField | z.ZodType) => z.ZodType;
|
|
88
|
+
|
|
89
|
+
export { type AnyField, type FlagsOf, type InnerOf, SFieldBase, type SchemaOf, objectFieldsRegistry, toZod };
|
package/lib/authoring.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// src/authoring.ts
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
var objectFieldsRegistry = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
var SFieldBase = class {
|
|
5
|
+
constructor(schema, native) {
|
|
6
|
+
this.schema = schema;
|
|
7
|
+
this.native = native;
|
|
8
|
+
}
|
|
9
|
+
// --- Field-level codec (raw, on `this.schema`): `decode` reads (wire -> app), `encode` writes
|
|
10
|
+
// (app -> wire). Create-shaping is a table concept, so these are NOT create-shaped. ---
|
|
11
|
+
/** Decode a DB value to its app type (wire -> app). */
|
|
12
|
+
decode(value) {
|
|
13
|
+
return z.decode(this.schema, value);
|
|
14
|
+
}
|
|
15
|
+
/** Encode an app value to its DB wire type (app -> wire). */
|
|
16
|
+
encode(value) {
|
|
17
|
+
return z.encode(this.schema, value);
|
|
18
|
+
}
|
|
19
|
+
decodeAsync(value) {
|
|
20
|
+
return z.decodeAsync(this.schema, value);
|
|
21
|
+
}
|
|
22
|
+
encodeAsync(value) {
|
|
23
|
+
return z.encodeAsync(this.schema, value);
|
|
24
|
+
}
|
|
25
|
+
safeDecode(value) {
|
|
26
|
+
return z.safeDecode(this.schema, value);
|
|
27
|
+
}
|
|
28
|
+
safeEncode(value) {
|
|
29
|
+
return z.safeEncode(this.schema, value);
|
|
30
|
+
}
|
|
31
|
+
safeDecodeAsync(value) {
|
|
32
|
+
return z.safeDecodeAsync(this.schema, value);
|
|
33
|
+
}
|
|
34
|
+
safeEncodeAsync(value) {
|
|
35
|
+
return z.safeEncodeAsync(this.schema, value);
|
|
36
|
+
}
|
|
37
|
+
// Deprecated Zod-style aliases — `parse` runs the DECODE direction (wire -> app).
|
|
38
|
+
/** @deprecated `parse` decodes a value (wire -> app). Use {@link decode}. */
|
|
39
|
+
parse(value) {
|
|
40
|
+
return this.decode(value);
|
|
41
|
+
}
|
|
42
|
+
/** @deprecated Use {@link safeDecode}. */
|
|
43
|
+
safeParse(value) {
|
|
44
|
+
return this.safeDecode(value);
|
|
45
|
+
}
|
|
46
|
+
/** @deprecated Use {@link decodeAsync}. */
|
|
47
|
+
parseAsync(value) {
|
|
48
|
+
return this.decodeAsync(value);
|
|
49
|
+
}
|
|
50
|
+
/** @deprecated Use {@link safeDecodeAsync}. */
|
|
51
|
+
safeParseAsync(value) {
|
|
52
|
+
return this.safeDecodeAsync(value);
|
|
53
|
+
}
|
|
54
|
+
// Zod wrappers — delegate to the inner schema, carry native metadata + flags forward.
|
|
55
|
+
optional() {
|
|
56
|
+
return this.rebuild(this.schema.optional(), this.native);
|
|
57
|
+
}
|
|
58
|
+
nullable() {
|
|
59
|
+
return this.rebuild(this.schema.nullable(), this.native);
|
|
60
|
+
}
|
|
61
|
+
default(value) {
|
|
62
|
+
return this.rebuild(this.schema.default(value), this.native);
|
|
63
|
+
}
|
|
64
|
+
/** Zod prefault: fill an absent value with `value`, then validate it (unlike `.default`). */
|
|
65
|
+
prefault(value) {
|
|
66
|
+
return this.rebuild(z.prefault(this.schema, value), this.native);
|
|
67
|
+
}
|
|
68
|
+
/** Zod catch: fall back to `value` when parsing fails. */
|
|
69
|
+
catch(value) {
|
|
70
|
+
return this.rebuild(this.schema.catch(value), this.native);
|
|
71
|
+
}
|
|
72
|
+
array() {
|
|
73
|
+
return this.rebuild(z.array(this.schema), this.native);
|
|
74
|
+
}
|
|
75
|
+
nullish() {
|
|
76
|
+
return this.rebuild(this.schema.nullish(), this.native);
|
|
77
|
+
}
|
|
78
|
+
/** Zod union — `a.or(b)` accepts either. Mirrors Zod's `.or()`. */
|
|
79
|
+
or(other) {
|
|
80
|
+
return this.rebuild(
|
|
81
|
+
z.union([this.schema, toZod(other)]),
|
|
82
|
+
this.blank()
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
/** Zod intersection — `a.and(b)`. Mirrors Zod's `.and()`. */
|
|
86
|
+
and(other) {
|
|
87
|
+
return this.rebuild(
|
|
88
|
+
z.intersection(this.schema, toZod(other)),
|
|
89
|
+
this.blank()
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
// --- Native Zod passthrough (drop-in for `z.*`): app-side validation / transform / metadata,
|
|
93
|
+
// delegated to the inner schema. The dialect-DDL side stays under the driver's `$`-methods. ---
|
|
94
|
+
refine(check, params) {
|
|
95
|
+
return this.rebuild(
|
|
96
|
+
this.schema.refine(check, params),
|
|
97
|
+
this.native
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
superRefine(refinement) {
|
|
101
|
+
return this.rebuild(
|
|
102
|
+
this.schema.superRefine(refinement),
|
|
103
|
+
this.native
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
check(...checks) {
|
|
107
|
+
return this.rebuild(
|
|
108
|
+
this.schema.check(...checks),
|
|
109
|
+
this.native
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
overwrite(fn) {
|
|
113
|
+
return this.rebuild(
|
|
114
|
+
this.schema.overwrite(fn),
|
|
115
|
+
this.native
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
brand(value) {
|
|
119
|
+
return this.rebuild(
|
|
120
|
+
this.schema.brand(value),
|
|
121
|
+
this.native
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
/** Zod's app-side metadata (JSON-schema/docs) — distinct from a driver's `$comment()`. */
|
|
125
|
+
describe(description) {
|
|
126
|
+
return this.rebuild(
|
|
127
|
+
this.schema.describe(description),
|
|
128
|
+
this.native
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
meta(data) {
|
|
132
|
+
return this.rebuild(
|
|
133
|
+
this.schema.meta(data),
|
|
134
|
+
this.native
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
/** Zod's app-side readonly (TS-immutable output) — distinct from a driver's `$readonly()`. */
|
|
138
|
+
readonly() {
|
|
139
|
+
return this.rebuild(this.schema.readonly(), this.native);
|
|
140
|
+
}
|
|
141
|
+
/** Zod transform — changes the decoded `App<>` value; the stored (wire) type is unchanged. */
|
|
142
|
+
transform(fn) {
|
|
143
|
+
return this.rebuild(this.schema.transform(fn), this.native);
|
|
144
|
+
}
|
|
145
|
+
/** Zod pipe — feed this field's output into `target`; the stored (wire) type stays `this`. */
|
|
146
|
+
pipe(target) {
|
|
147
|
+
return this.rebuild(
|
|
148
|
+
this.schema.pipe(target),
|
|
149
|
+
this.native
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
/** Peel one wrapper (optional/nullable/default/prefault/catch/readonly/array) off the field. */
|
|
153
|
+
unwrap() {
|
|
154
|
+
const def = this.schema._zod.def;
|
|
155
|
+
const inner = def.innerType ?? def.element ?? this.schema;
|
|
156
|
+
return this.rebuild(inner, this.native);
|
|
157
|
+
}
|
|
158
|
+
/** Object-only: allow arbitrary extra keys — `FLEXIBLE` in DDL. Mirrors Zod's `.loose()`. */
|
|
159
|
+
loose() {
|
|
160
|
+
return this.objectMode("loose");
|
|
161
|
+
}
|
|
162
|
+
/** Object-only: reject unknown keys — the default. Mirrors Zod's `.strict()`. */
|
|
163
|
+
strict() {
|
|
164
|
+
return this.objectMode("strict");
|
|
165
|
+
}
|
|
166
|
+
/** Alias for {@link loose} — a `FLEXIBLE` object accepting arbitrary keys. */
|
|
167
|
+
flexible() {
|
|
168
|
+
return this.loose();
|
|
169
|
+
}
|
|
170
|
+
objectMode(mode) {
|
|
171
|
+
const obj = this.schema;
|
|
172
|
+
if (typeof obj.loose !== "function" || typeof obj.strict !== "function") {
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
const next = mode === "loose" ? obj.loose() : obj.strict();
|
|
176
|
+
const fields = objectFieldsRegistry.get(this.schema);
|
|
177
|
+
if (fields) objectFieldsRegistry.set(next, fields);
|
|
178
|
+
return this.rebuild(next, this.native);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var toZod = (v) => v instanceof SFieldBase ? v.schema : v;
|
|
182
|
+
export {
|
|
183
|
+
SFieldBase,
|
|
184
|
+
objectFieldsRegistry,
|
|
185
|
+
toZod
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=authoring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/authoring.ts"],"sourcesContent":["// The NEUTRAL, dialect-agnostic AUTHORING BASE (docs/AUTHORING-SPLIT.md — \"base builder in core\").\n// Each driver package builds its `s.*` on this: `class <D>Field extends SFieldBase<S, Flags, <D>Meta>`\n// adds the dialect's native authoring (`$`-methods) and its `$<driver>(type, codec)` escape hatch for\n// types not representable on the wire; the base provides the Zod codec, the Zod wrappers, the full\n// `z.*` passthrough, and the `rebuild`/`blank` seam that carries native metadata through a chain.\n//\n// It references NOTHING dialect-specific — it's generic over the per-dialect native-metadata slot `N`.\n// It is also Zod-CLEAN: app-side behaviour delegates to the inner Zod schema (`z.decode`/`z.encode`/\n// the wrappers) via Zod's public API, with side-channel metadata kept on WeakMaps — never patching\n// Zod internals.\n\nimport * as z from \"zod\";\n\n// `SFieldBase` is INVARIANT in its native-metadata slot `N` (the protected `rebuild(native: N)` makes\n// N contravariant while `native`/`blank` make it covariant). So a dialect field — `SField` with\n// `N = SurrealMeta` — is NOT assignable to a fixed `N = unknown`, which would make `AnyField` reject\n// real dialect fields (e.g. `.or(s.int())`). At THIS cross-dialect boundary `N` is honestly \"any\n// dialect's metadata\": erase it to `any` (bivariant) so every driver's field is an `AnyField`. The\n// concrete `N` is preserved everywhere it matters — each driver's own field type keeps `N = <D>Meta`.\n\n/** Any field of ANY dialect — the base type the helpers + wrappers accept. */\n// biome-ignore lint/suspicious/noExplicitAny: cross-dialect erasure of the invariant native slot N.\nexport type AnyField = SFieldBase<z.ZodType, string, any>;\n\n/** The Zod schema a field (or a raw Zod schema) carries. */\nexport type SchemaOf<F> =\n // biome-ignore lint/suspicious/noExplicitAny: match a field of any dialect (N is invariant).\n F extends SFieldBase<infer S, string, any>\n ? S\n : F extends z.ZodType\n ? F\n : never;\n\n/** The `Flags` channel a field carries (driver `$`-methods brand it; widens to `string` for `Shape`). */\nexport type FlagsOf<F> =\n // biome-ignore lint/suspicious/noExplicitAny: match a field of any dialect (N is invariant).\n F extends SFieldBase<z.ZodType, infer Fl, any> ? Fl : never;\n\n/** The schema one wrapper down — what `unwrap()` returns. */\nexport type InnerOf<S extends z.ZodType> =\n S extends z.ZodOptional<infer I extends z.ZodType>\n ? I\n : S extends z.ZodNullable<infer I extends z.ZodType>\n ? I\n : S extends z.ZodDefault<infer I extends z.ZodType>\n ? I\n : S extends z.ZodPrefault<infer I extends z.ZodType>\n ? I\n : S extends z.ZodCatch<infer I extends z.ZodType>\n ? I\n : S extends z.ZodReadonly<infer I extends z.ZodType>\n ? I\n : S extends z.ZodArray<infer I extends z.ZodType>\n ? I\n : S;\n\n/**\n * Maps an object schema (built via a driver's `s.object`) to its original field shape, so nested\n * fields keep their authoring metadata through generation. Kept on the schema, not the field, so it\n * composes through `array()`/`optional()`/nesting.\n */\nexport const objectFieldsRegistry = new WeakMap<\n z.ZodType,\n Record<string, AnyField>\n>();\n\n/**\n * The PORTABLE, dialect-agnostic field base. Holds the Zod schema, an opaque per-dialect `native`\n * metadata slot, the field-level codecs, and the app-land Zod wrappers (which carry `native` forward\n * via the `rebuild` hook so a chain keeps its concrete dialect type). Each dialect subclasses it to\n * add native authoring (`$`-methods) and re-type the wrappers so a chain stays its own field type.\n */\nexport abstract class SFieldBase<\n S extends z.ZodType = z.ZodType,\n Flags extends string = never,\n N = unknown,\n> {\n constructor(\n readonly schema: S,\n readonly native: N,\n ) {}\n\n /** Rebuild a sibling field of the SAME dialect with a new schema/flags. Each dialect overrides it. */\n protected abstract rebuild<S2 extends z.ZodType, F2 extends string>(\n schema: S2,\n native: N,\n ): SFieldBase<S2, F2, N>;\n /** A fresh, empty native-metadata bag (for wrappers like `or`/`and` that reset it). */\n protected abstract blank(): N;\n\n // --- Field-level codec (raw, on `this.schema`): `decode` reads (wire -> app), `encode` writes\n // (app -> wire). Create-shaping is a table concept, so these are NOT create-shaped. ---\n /** Decode a DB value to its app type (wire -> app). */\n decode(value: unknown): z.output<S> {\n return z.decode(this.schema, value as never);\n }\n /** Encode an app value to its DB wire type (app -> wire). */\n encode(value: z.output<S>): z.input<S> {\n return z.encode(this.schema, value);\n }\n decodeAsync(value: unknown): Promise<z.output<S>> {\n return z.decodeAsync(this.schema, value as never);\n }\n encodeAsync(value: z.output<S>): Promise<z.input<S>> {\n return z.encodeAsync(this.schema, value);\n }\n safeDecode(value: unknown) {\n return z.safeDecode(this.schema, value as never);\n }\n safeEncode(value: z.output<S>) {\n return z.safeEncode(this.schema, value);\n }\n safeDecodeAsync(value: unknown) {\n return z.safeDecodeAsync(this.schema, value as never);\n }\n safeEncodeAsync(value: z.output<S>) {\n return z.safeEncodeAsync(this.schema, value);\n }\n // Deprecated Zod-style aliases — `parse` runs the DECODE direction (wire -> app).\n /** @deprecated `parse` decodes a value (wire -> app). Use {@link decode}. */\n parse(value: unknown): z.output<S> {\n return this.decode(value);\n }\n /** @deprecated Use {@link safeDecode}. */\n safeParse(value: unknown) {\n return this.safeDecode(value);\n }\n /** @deprecated Use {@link decodeAsync}. */\n parseAsync(value: unknown): Promise<z.output<S>> {\n return this.decodeAsync(value);\n }\n /** @deprecated Use {@link safeDecodeAsync}. */\n safeParseAsync(value: unknown) {\n return this.safeDecodeAsync(value);\n }\n\n // Zod wrappers — delegate to the inner schema, carry native metadata + flags forward.\n optional(): SFieldBase<z.ZodOptional<S>, Flags, N> {\n return this.rebuild(this.schema.optional(), this.native);\n }\n nullable(): SFieldBase<z.ZodNullable<S>, Flags, N> {\n return this.rebuild(this.schema.nullable(), this.native);\n }\n default(value: z.input<S>): SFieldBase<z.ZodDefault<S>, Flags, N> {\n return this.rebuild(this.schema.default(value as never), this.native);\n }\n /** Zod prefault: fill an absent value with `value`, then validate it (unlike `.default`). */\n prefault(value: z.input<S>): SFieldBase<z.ZodPrefault<S>, Flags, N> {\n return this.rebuild(z.prefault(this.schema, value as never), this.native);\n }\n /** Zod catch: fall back to `value` when parsing fails. */\n catch(value: z.output<S>): SFieldBase<z.ZodCatch<S>, Flags, N> {\n return this.rebuild(this.schema.catch(value as never), this.native);\n }\n array(): SFieldBase<z.ZodArray<S>, Flags, N> {\n return this.rebuild(z.array(this.schema), this.native);\n }\n nullish(): SFieldBase<z.ZodOptional<z.ZodNullable<S>>, Flags, N> {\n return this.rebuild(this.schema.nullish(), this.native);\n }\n /** Zod union — `a.or(b)` accepts either. Mirrors Zod's `.or()`. */\n or<F extends AnyField | z.ZodType>(\n other: F,\n ): SFieldBase<z.ZodUnion<[S, SchemaOf<F>]>, never, N> {\n return this.rebuild<z.ZodUnion<[S, SchemaOf<F>]>, never>(\n z.union([this.schema, toZod(other)]) as z.ZodUnion<[S, SchemaOf<F>]>,\n this.blank(),\n );\n }\n /** Zod intersection — `a.and(b)`. Mirrors Zod's `.and()`. */\n and<F extends AnyField | z.ZodType>(\n other: F,\n ): SFieldBase<z.ZodIntersection<S, SchemaOf<F>>, never, N> {\n return this.rebuild<z.ZodIntersection<S, SchemaOf<F>>, never>(\n z.intersection(this.schema, toZod(other) as SchemaOf<F>),\n this.blank(),\n );\n }\n\n // --- Native Zod passthrough (drop-in for `z.*`): app-side validation / transform / metadata,\n // delegated to the inner schema. The dialect-DDL side stays under the driver's `$`-methods. ---\n refine(\n check: (arg: z.output<S>) => unknown,\n params?: string | z.core.$ZodCustomParams,\n ): this {\n return this.rebuild(\n this.schema.refine(check, params) as S,\n this.native,\n ) as unknown as this;\n }\n superRefine(\n refinement: (\n arg: z.output<S>,\n ctx: z.core.$RefinementCtx<z.output<S>>,\n ) => void,\n ): this {\n return this.rebuild(\n this.schema.superRefine(refinement) as S,\n this.native,\n ) as unknown as this;\n }\n check(\n ...checks: (z.core.CheckFn<z.output<S>> | z.core.$ZodCheck<z.output<S>>)[]\n ): this {\n return this.rebuild(\n this.schema.check(...checks) as S,\n this.native,\n ) as unknown as this;\n }\n overwrite(fn: (x: z.output<S>) => z.output<S>): this {\n return this.rebuild(\n this.schema.overwrite(fn) as S,\n this.native,\n ) as unknown as this;\n }\n brand<B extends PropertyKey = PropertyKey>(value?: B): this {\n return this.rebuild(\n this.schema.brand(value) as unknown as S,\n this.native,\n ) as unknown as this;\n }\n /** Zod's app-side metadata (JSON-schema/docs) — distinct from a driver's `$comment()`. */\n describe(description: string): this {\n return this.rebuild(\n this.schema.describe(description) as S,\n this.native,\n ) as unknown as this;\n }\n meta(data: z.core.GlobalMeta): this {\n return this.rebuild(\n this.schema.meta(data) as S,\n this.native,\n ) as unknown as this;\n }\n /** Zod's app-side readonly (TS-immutable output) — distinct from a driver's `$readonly()`. */\n readonly(): SFieldBase<z.ZodReadonly<S>, Flags, N> {\n return this.rebuild(this.schema.readonly(), this.native);\n }\n /** Zod transform — changes the decoded `App<>` value; the stored (wire) type is unchanged. */\n transform<NewOut>(\n fn: (arg: z.output<S>, ctx: z.core.$RefinementCtx<z.output<S>>) => NewOut,\n ): SFieldBase<\n z.ZodPipe<S, z.ZodTransform<Awaited<NewOut>, z.output<S>>>,\n Flags,\n N\n > {\n return this.rebuild(this.schema.transform(fn), this.native);\n }\n /** Zod pipe — feed this field's output into `target`; the stored (wire) type stays `this`. */\n pipe<T extends z.core.$ZodType<unknown, z.output<S>>>(\n target: T,\n ): SFieldBase<z.ZodPipe<S, T>, Flags, N> {\n return this.rebuild(\n this.schema.pipe(target) as z.ZodPipe<S, T>,\n this.native,\n );\n }\n /** Peel one wrapper (optional/nullable/default/prefault/catch/readonly/array) off the field. */\n unwrap(): SFieldBase<InnerOf<S>, Flags, N> {\n const def = this.schema._zod.def as {\n innerType?: z.ZodType;\n element?: z.ZodType;\n };\n const inner = def.innerType ?? def.element ?? this.schema;\n return this.rebuild(inner, this.native) as unknown as SFieldBase<\n InnerOf<S>,\n Flags,\n N\n >;\n }\n\n /** Object-only: allow arbitrary extra keys — `FLEXIBLE` in DDL. Mirrors Zod's `.loose()`. */\n loose(): this {\n return this.objectMode(\"loose\");\n }\n /** Object-only: reject unknown keys — the default. Mirrors Zod's `.strict()`. */\n strict(): this {\n return this.objectMode(\"strict\");\n }\n /** Alias for {@link loose} — a `FLEXIBLE` object accepting arbitrary keys. */\n flexible(): this {\n return this.loose();\n }\n private objectMode(mode: \"loose\" | \"strict\"): this {\n const obj = this.schema as unknown as {\n loose?: () => z.ZodType;\n strict?: () => z.ZodType;\n };\n if (typeof obj.loose !== \"function\" || typeof obj.strict !== \"function\") {\n return this; // not an object schema — no-op\n }\n const next = (mode === \"loose\"\n ? obj.loose()\n : obj.strict()) as unknown as S;\n // Carry the nested-field registry forward so DDL/create-shaping still see the subfields.\n const fields = objectFieldsRegistry.get(this.schema);\n if (fields) objectFieldsRegistry.set(next, fields);\n return this.rebuild(next, this.native) as unknown as this;\n }\n}\n\n/** Unwrap a field to its Zod schema (raw Zod schemas pass through). */\nexport const toZod = (v: AnyField | z.ZodType): z.ZodType =>\n v instanceof SFieldBase ? v.schema : v;\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/connection.ts
|
|
2
|
+
function connectionEntry(driver, input) {
|
|
3
|
+
return {
|
|
4
|
+
__schemic: "connection",
|
|
5
|
+
driver,
|
|
6
|
+
async resolve(ctx) {
|
|
7
|
+
const out = typeof input === "function" ? await input(ctx) : input;
|
|
8
|
+
return Array.isArray(out) ? out : [out];
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function isConnectionEntry(v) {
|
|
13
|
+
return typeof v === "object" && v !== null && v.__schemic === "connection";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/driver/portable.ts
|
|
17
|
+
var scalar = (name) => ({
|
|
18
|
+
t: "scalar",
|
|
19
|
+
name
|
|
20
|
+
});
|
|
21
|
+
var literal = (value) => ({
|
|
22
|
+
t: "literal",
|
|
23
|
+
value
|
|
24
|
+
});
|
|
25
|
+
var option = (inner) => inner.t === "scalar" && inner.name === "any" ? inner : { t: "option", inner };
|
|
26
|
+
var nullable = (inner) => {
|
|
27
|
+
if (inner.t === "scalar" && inner.name === "any") return inner;
|
|
28
|
+
if (inner.t === "option")
|
|
29
|
+
return { t: "option", inner: nullable(inner.inner) };
|
|
30
|
+
return { t: "nullable", inner };
|
|
31
|
+
};
|
|
32
|
+
var array = (elem, size) => ({
|
|
33
|
+
t: "array",
|
|
34
|
+
elem,
|
|
35
|
+
...size !== void 0 ? { size } : {}
|
|
36
|
+
});
|
|
37
|
+
var union = (members) => members.length === 1 ? members[0] : { t: "union", members };
|
|
38
|
+
var record = (tables) => ({
|
|
39
|
+
t: "record",
|
|
40
|
+
tables
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
connectionEntry,
|
|
45
|
+
isConnectionEntry,
|
|
46
|
+
scalar,
|
|
47
|
+
literal,
|
|
48
|
+
option,
|
|
49
|
+
nullable,
|
|
50
|
+
array,
|
|
51
|
+
union,
|
|
52
|
+
record
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=chunk-C4D6JWSE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/connection.ts","../src/driver/portable.ts"],"sourcesContent":["// The neutral MULTI-CONNECTION contract (design: docs/MULTI-CONNECTION.md). A project's config maps\n// names to CONNECTIONS; each is produced by a per-driver `<driver>Connection(...)` factory that wraps\n// {@link connectionEntry} with its own typed connection shape. Everything here is dialect-free — the\n// CLI reads only these neutral fields; driver-specific connection params ride on the driver's own\n// config type. The resolution engine (lazy DAG, fan-out, addressing) lives in the CLI layer.\n\ntype MaybePromise<T> = T | Promise<T>;\n\n/** The dialect-neutral fields the orchestration reads off every connection config. */\nexport interface ConnectionConfigBase {\n /** Schema dir (the desired state + its migration files/snapshot). Shared dir = shared schema. */\n schema: string;\n /** Address within a COLLECTION (array-returning resolver) — `<name>:<key>`. Required on array entries. */\n key?: string;\n /** Migrations dir override; defaults relative to `schema`. */\n migrations?: string;\n}\n\n/** A live, queryable handle to ANOTHER (already-resolved) connection, for use inside a resolver. */\nexport interface ResolvedConnectionHandle {\n query<T = unknown>(sql: string, vars?: Record<string, unknown>): Promise<T[]>;\n}\n\n/**\n * What a connection RESOLVER receives. `connections` is a LAZY proxy of the other connections —\n * touching one resolves + connects it on demand (so the dependency graph falls out of access; cycles\n * error). `args` are CLI `--arg k=v` values (so a resolver can yield a SUBSET without resolving all).\n */\nexport interface ResolveContext {\n connections: Record<string, ResolvedConnectionHandle>;\n args: Record<string, string>;\n env: NodeJS.ProcessEnv;\n}\n\n/**\n * The opaque, branded output of a `<driver>Connection(...)` factory — the only thing `defineConfig`'s\n * `connections` map accepts. Never hand-authored. `driver` is the package the CLI dynamically loads;\n * `resolve` always normalizes to an ARRAY (a single connection -> one element, a collection -> many).\n */\nexport interface ConnectionEntry {\n readonly __schemic: \"connection\";\n readonly driver: string;\n resolve(ctx: ResolveContext): Promise<ConnectionConfigBase[]>;\n}\n\n/** A connection factory's input: a static config, or a resolver yielding one config or a keyed collection. */\nexport type ConnectionInput<C extends ConnectionConfigBase> =\n | C\n | ((ctx: ResolveContext) => MaybePromise<C | (C & { key: string })[]>);\n\n/**\n * Build a {@link ConnectionEntry} from a driver tag + a static config or resolver — the primitive each\n * driver package wraps in its typed `<driver>Connection(...)` factory (which fixes `C` to the driver's\n * own connection shape and overloads the array form to require `key`). Returns a branded entry whose\n * `resolve` always yields an array.\n */\nexport function connectionEntry<C extends ConnectionConfigBase>(\n driver: string,\n input: ConnectionInput<C>,\n): ConnectionEntry {\n return {\n __schemic: \"connection\",\n driver,\n async resolve(ctx) {\n const out = typeof input === \"function\" ? await input(ctx) : input;\n return Array.isArray(out) ? out : [out];\n },\n };\n}\n\n/** Type guard: is a `connections` map value a real factory output (vs a stray object)? */\nexport function isConnectionEntry(v: unknown): v is ConnectionEntry {\n return (\n typeof v === \"object\" &&\n v !== null &&\n (v as { __schemic?: unknown }).__schemic === \"connection\"\n );\n}\n","// The PORTABLE TYPE MODEL — the keystone of multi-DB support (see docs/MULTI-DB-SPIKE.md).\n//\n// Today the Struct-IR carries a field's type as a SurrealQL type-expression STRING\n// (`StructField.kind`, e.g. `option<int>`, `array<record<user>, 3>`). That string is a dialect leak:\n// equality and rendering both have to understand SurrealQL grammar. This module defines the\n// dialect-INDEPENDENT replacement — a structured type that every driver translates to/from.\n//\n// STATUS (Milestone 1): defined but NOT yet wired into `StructField`. Milestone 2 replaces\n// `kind: string` with `type: PortableType`, makes `inferField` (src/ddl.ts) produce it, and flips\n// diff equality to a structured deep-compare over the normalized IR. Until then this is the target\n// shape, kept in code so the Surreal driver's `emitType`/`parseType` can be built against it.\n\n/**\n * The portable scalar set — the common denominator every driver maps to a concrete column type and\n * back. A driver MAY reject scalars it cannot represent (and authoring can pin a richer DB-native\n * type via `{ t: \"native\" }`). Names are lowercase and dialect-neutral.\n */\nexport type ScalarName =\n | \"any\"\n | \"bool\"\n | \"string\"\n | \"int\"\n | \"float\"\n | \"decimal\"\n | \"number\" // a numeric whose int/float/decimal-ness is unconstrained\n | \"datetime\"\n | \"duration\"\n | \"uuid\"\n | \"bytes\"\n | \"null\"; // the unit type of SQL NULL / Surreal `null` (distinct from `option`'s absence)\n\n/**\n * A dialect-independent field type. Drivers translate this to their own type expression (`emitType`)\n * and parse their introspection back into it (`parseType`). `option` and `nullable` are ORTHOGONAL\n * and BOTH equality-relevant — see the note on `nullable` below; never collapse them.\n */\nexport type PortableType =\n /** A primitive scalar. */\n | { t: \"scalar\"; name: ScalarName }\n /** A literal value type, e.g. the `'active'` in `'active' | 'archived'`. */\n | { t: \"literal\"; value: string | number | boolean }\n /**\n * The field may be ABSENT / NONE (Surreal `option<T>`; SQL \"column omitted / has a DEFAULT\").\n * Orthogonal to `nullable`.\n */\n | { t: \"option\"; inner: PortableType }\n /**\n * The field may be NULL (Surreal `T | null`; SQL `NULL` vs `NOT NULL`). Orthogonal to `option`:\n * `option<T>`, `T | null`, and `option<T | null>` are THREE DISTINCT types. `normalize()` folds\n * `nullable(option(X))` -> `option(nullable(X))` so `.optional().nullable()` ≡ `.nullish()`.\n */\n | { t: \"nullable\"; inner: PortableType }\n /** An ordered, possibly length-bounded list. */\n | { t: \"array\"; elem: PortableType; size?: number }\n /** A set (distinct elements), possibly length-bounded. */\n | { t: \"set\"; elem: PortableType; size?: number }\n /** A discriminated/plain union. `normalize()` keeps `members` canonically sorted. */\n | { t: \"union\"; members: PortableType[] }\n /** A nested object/record literal. `flexible` allows undeclared keys (Surreal FLEXIBLE). */\n | { t: \"object\"; fields: Record<string, PortableType>; flexible?: boolean }\n /**\n * A link to a row in one of `tables` (Surreal `record<a | b>`; SQL foreign key). The id-VALUE type\n * is intentionally NOT modelled here — the DDL never encodes it; it lives App/Wire-side (TS-only).\n */\n | { t: \"record\"; tables: string[] }\n /** A geometry type (Surreal-native; PostGIS or unsupported elsewhere). */\n | { t: \"geometry\"; kind: GeometryKind }\n /** The bottom type (no value). */\n | { t: \"never\" }\n /**\n * An escape hatch for a DB-specific type with no portable meaning (PG `tsvector`, etc.). Carries\n * the owning driver `db` so a schema authored for one DB can't silently typecheck against another.\n * `params` carries the type's parameters for parameterized natives — `numeric(p,s)`, `varchar(n)`,\n * `timestamp(p)` — so a driver round-trips `numeric(10,2)` exactly. Order matters; ignored when empty.\n */\n | { t: \"native\"; db: string; name: string; params?: (string | number)[] };\n\nexport type GeometryKind =\n | \"feature\"\n | \"point\"\n | \"line\"\n | \"polygon\"\n | \"multipoint\"\n | \"multiline\"\n | \"multipolygon\"\n | \"collection\";\n\n// --- Constructors (ergonomic, and a single place to enforce the fold invariants) ----------------\n\nexport const scalar = (name: ScalarName): PortableType => ({\n t: \"scalar\",\n name,\n});\nexport const literal = (value: string | number | boolean): PortableType => ({\n t: \"literal\",\n value,\n});\n\n/** `option<T>` — but `option<any>` collapses to `any` (any already admits NONE), matching ddl.ts. */\nexport const option = (inner: PortableType): PortableType =>\n inner.t === \"scalar\" && inner.name === \"any\" ? inner : { t: \"option\", inner };\n\n/**\n * `T | null` — with the fold rule `nullable(option(X))` -> `option(nullable(X))` so\n * `.optional().nullable()` ≡ `.nullish()`, and `nullable(any)` collapses to `any`.\n */\nexport const nullable = (inner: PortableType): PortableType => {\n if (inner.t === \"scalar\" && inner.name === \"any\") return inner;\n if (inner.t === \"option\")\n return { t: \"option\", inner: nullable(inner.inner) };\n return { t: \"nullable\", inner };\n};\n\nexport const array = (elem: PortableType, size?: number): PortableType => ({\n t: \"array\",\n elem,\n ...(size !== undefined ? { size } : {}),\n});\nexport const union = (members: PortableType[]): PortableType =>\n members.length === 1 ? members[0] : { t: \"union\", members };\nexport const record = (tables: string[]): PortableType => ({\n t: \"record\",\n tables,\n});\n"],"mappings":";AAwDO,SAAS,gBACd,QACA,OACiB;AACjB,SAAO;AAAA,IACL,WAAW;AAAA,IACX;AAAA,IACA,MAAM,QAAQ,KAAK;AACjB,YAAM,MAAM,OAAO,UAAU,aAAa,MAAM,MAAM,GAAG,IAAI;AAC7D,aAAO,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,GAAG;AAAA,IACxC;AAAA,EACF;AACF;AAGO,SAAS,kBAAkB,GAAkC;AAClE,SACE,OAAO,MAAM,YACb,MAAM,QACL,EAA8B,cAAc;AAEjD;;;ACYO,IAAM,SAAS,CAAC,UAAoC;AAAA,EACzD,GAAG;AAAA,EACH;AACF;AACO,IAAM,UAAU,CAAC,WAAoD;AAAA,EAC1E,GAAG;AAAA,EACH;AACF;AAGO,IAAM,SAAS,CAAC,UACrB,MAAM,MAAM,YAAY,MAAM,SAAS,QAAQ,QAAQ,EAAE,GAAG,UAAU,MAAM;AAMvE,IAAM,WAAW,CAAC,UAAsC;AAC7D,MAAI,MAAM,MAAM,YAAY,MAAM,SAAS,MAAO,QAAO;AACzD,MAAI,MAAM,MAAM;AACd,WAAO,EAAE,GAAG,UAAU,OAAO,SAAS,MAAM,KAAK,EAAE;AACrD,SAAO,EAAE,GAAG,YAAY,MAAM;AAChC;AAEO,IAAM,QAAQ,CAAC,MAAoB,UAAiC;AAAA,EACzE,GAAG;AAAA,EACH;AAAA,EACA,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AACvC;AACO,IAAM,QAAQ,CAAC,YACpB,QAAQ,WAAW,IAAI,QAAQ,CAAC,IAAI,EAAE,GAAG,SAAS,QAAQ;AACrD,IAAM,SAAS,CAAC,YAAoC;AAAA,EACzD,GAAG;AAAA,EACH;AACF;","names":[]}
|