@supatype/types 0.1.0-alpha.9

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/index.ts ADDED
@@ -0,0 +1,496 @@
1
+ import type { SerializedEditorState } from "./lexical.js"
2
+
3
+ declare const SUPATYPE_TYPE: unique symbol
4
+ declare const SUPATYPE_MODEL: unique symbol
5
+
6
+ type Brand<TShape, TTag extends string> = TShape & {
7
+ readonly [SUPATYPE_TYPE]?: { readonly tag: TTag }
8
+ }
9
+
10
+ type Primitive<TName extends string, TShape> = Brand<TShape, `primitive:${TName}`>
11
+ type Modifier<TName extends string, TInner> = Brand<TInner, `modifier:${TName}`>
12
+ type Relation<TName extends string, TInner> = Brand<TInner, `relation:${TName}`>
13
+ type Access<TName extends string, TShape = { readonly kind: TName }> = Brand<TShape, `access:${TName}`>
14
+
15
+ export type UUID = Primitive<"UUID", string>
16
+ export type Email = Primitive<"Email", string>
17
+ export type URL = Primitive<"URL", string>
18
+ /**
19
+ * URL-safe slug stored as text. Optional type argument names the source field used
20
+ * for auto-generation (DB trigger + Studio). Defaults to `"title"`.
21
+ */
22
+ export type Slug<TSource extends string = "title"> = Primitive<`Slug:${TSource}`, string>
23
+ export type PhoneNumber = Primitive<"PhoneNumber", string>
24
+ export type Markdown = Primitive<"Markdown", string>
25
+ export type Color = Primitive<"Color", string>
26
+ export type Int = Primitive<"Int", number>
27
+ export type SmallInt = Primitive<"SmallInt", number>
28
+ export type BigInt = Primitive<"BigInt", bigint>
29
+ export type Float = Primitive<"Float", number>
30
+ export type Decimal<P extends number, S extends number> = Primitive<`Decimal:${P}:${S}`, string>
31
+ export type Money = Primitive<"Money", string>
32
+ export type Vector<N extends number> = Primitive<`Vector:${N}`, number[]>
33
+ export type JSON<T> = Primitive<"JSON", T>
34
+ export type IPAddress = Primitive<"IPAddress", string>
35
+ export type CIDR = Primitive<"CIDR", string>
36
+ export type MacAddress = Primitive<"MacAddress", string>
37
+ export type Bytea = Primitive<"Bytea", string>
38
+ export type XML = Primitive<"XML", string>
39
+ export type TSQuery = Primitive<"TSQuery", string>
40
+ export type TSVector = Primitive<"TSVector", string>
41
+ export type DateOnly = Primitive<"DateOnly", Date>
42
+ export type DateTime = Primitive<"DateTime", Date>
43
+ export type Timestamp = Primitive<"Timestamp", Date>
44
+ export type Geo = Primitive<"Geo", { type: "point" | "polygon" | "linestring"; coordinates: unknown }>
45
+
46
+ export type Code<Lang extends string = string> = Primitive<"Code", { lang: Lang; source: string }>
47
+ export type Duration = Primitive<"Duration", { ms: number }>
48
+ export type GeoPoint = Primitive<"GeoPoint", { lat: number; lng: number }>
49
+ export type Currency<Code extends string = string> = Primitive<"Currency", { amount: bigint; code: Code }>
50
+
51
+ /** Link target for {@link Button}. */
52
+ export type ButtonTarget = "_self" | "_blank"
53
+
54
+ /** CMS button / CTA — label, href, optional aria-label and target (stored as JSONB). */
55
+ export type ButtonValue = {
56
+ label: string
57
+ href: string
58
+ ariaLabel?: string
59
+ target?: ButtonTarget
60
+ }
61
+
62
+ export type Button = Primitive<"Button", ButtonValue>
63
+ /**
64
+ * Lexical JSON in DB/UI.
65
+ * **`string`** is allowed in TS for defaults, seeds, and incremental adoption (plain text or Lexical JSON string — not HTML).
66
+ *
67
+ * Schema defaults:
68
+ * - `RichText<"Your sentence">` — plain-text default (sugar)
69
+ * - `Default<RichText, "Your sentence">` — same, composes with modifiers
70
+ * - Lexical document: pass a JSON string literal or use `Default<RichText, '{"root":…}'>`
71
+ */
72
+ export type RichText<D extends string = never> = Primitive<
73
+ D extends never ? "RichText" : `RichText:${D}`,
74
+ SerializedEditorState | string
75
+ >
76
+
77
+ /** Visibility / S3 coupling for a storage bucket (`storage.buckets` + optional `PutBucketPolicy`). */
78
+ export type BucketAccessMode = "public" | "private" | "custom"
79
+ export type BucketPublic = Access<"BucketPublic">
80
+ export type BucketPrivate = Access<"BucketPrivate">
81
+ export type BucketLoggedIn = Access<"BucketLoggedIn">
82
+ export type BucketOwner = Access<"BucketOwner">
83
+ export type BucketRole<R extends string = string> = Access<"BucketRole", {
84
+ readonly kind: "BucketRole"
85
+ readonly role: R
86
+ }>
87
+
88
+ /**
89
+ * Storage RLS subset: same primitives as {@link ModelMeta.access} (`read`, `create`, …) but typically
90
+ * only `read` / `create` / `delete` are used for `storage.objects` policies when set on the bucket.
91
+ */
92
+ export type BucketStorageAccess = {
93
+ read?: BucketPublic | BucketPrivate | BucketLoggedIn | BucketOwner | BucketRole<string>
94
+ create?: BucketPublic | BucketPrivate | BucketLoggedIn | BucketOwner | BucketRole<string>
95
+ delete?: BucketPublic | BucketPrivate | BucketLoggedIn | BucketOwner | BucketRole<string>
96
+ }
97
+
98
+ /**
99
+ * Second generic on {@link Bucket}. Parsed by `@supatype/cli` into `storage.buckets` rows and optional
100
+ * bucket-scoped storage RLS. String sizes (`maxSize`) use CLI parsing (e.g. `"50MB"`).
101
+ */
102
+ export type BucketConfig<
103
+ _TAccess extends BucketAccessMode | undefined = undefined,
104
+ _TMax extends string | undefined = undefined,
105
+ > = {
106
+ accessMode?: BucketAccessMode
107
+ maxSize?: string
108
+ accept?: readonly string[]
109
+ /** When set with `access`, drives `storage.objects` RLS for this bucket instead of model `access`. */
110
+ access?: BucketStorageAccess
111
+ /** Raw AWS S3 / MinIO bucket policy JSON string; when set, overrides the default policy for public/custom flows. */
112
+ s3BucketPolicy?: string
113
+ }
114
+
115
+ /**
116
+ * Names a logical storage bucket. Export `type AvatarBucket = Bucket<"avatars", { … }>` and pass
117
+ * {@link ImageAsset}<AvatarBucket> / {@link FileAsset}<AvatarBucket> on models.
118
+ *
119
+ * Config is erased at runtime — only `@supatype/cli` reads it via the TypeScript type checker shape.
120
+ */
121
+ export type Bucket<
122
+ TName extends string = string,
123
+ TConfig extends BucketConfig | Record<string, unknown> = BucketConfig,
124
+ > = Primitive<`Bucket:${TName}`, { name: TName; config?: TConfig }>
125
+ export type Asset<TBucket extends Bucket = Bucket> = Primitive<"Asset", {
126
+ bucket: TBucket
127
+ path: string
128
+ mimeType?: string
129
+ size?: number
130
+ }>
131
+ /** Second generic on {@link ImageAsset} / {@link FileAsset} — parsed by CLI only. */
132
+ export type AssetFieldOptions = {
133
+ /** When true, stored as JSONB locale map of storage refs. Default false. */
134
+ localized?: boolean
135
+ }
136
+
137
+ export type ImageAsset<
138
+ TBucket extends Bucket = Bucket,
139
+ TOptions extends AssetFieldOptions = {},
140
+ > = Primitive<"ImageAsset", Asset<TBucket> & {
141
+ width?: number
142
+ height?: number
143
+ config?: TOptions
144
+ }>
145
+ export type FileAsset<
146
+ TBucket extends Bucket = Bucket,
147
+ TOptions extends AssetFieldOptions = {},
148
+ > = Primitive<"FileAsset", Asset<TBucket> & { config?: TOptions }>
149
+
150
+ export type LocaleConfig<
151
+ TLocales extends readonly string[] = readonly string[],
152
+ TDefault extends TLocales[number] = TLocales[number],
153
+ > = Primitive<"LocaleConfig", { locales: TLocales; defaultLocale: TDefault }>
154
+ /**
155
+ * Translatable field — stored as JSONB with locale keys in Postgres,
156
+ * e.g. `{ "en": "Hello", "de": "Hallo" }`. Configure locales with {@link LocaleConfig}.
157
+ *
158
+ * Use `Optional<Localized<string>>` when the field may be null.
159
+ */
160
+ export type Localized<T> = Modifier<"Localized", Record<string, T>>
161
+ /** Opt out of {@link LocalizedModel} auto-localization for a copy-like field. */
162
+ export type NotLocalized<T> = Modifier<"NotLocalized", T>
163
+ export type Block<
164
+ TName extends string = string,
165
+ TFields extends Record<string, unknown> = Record<string, unknown>,
166
+ TMeta extends { label?: string; icon?: string } = {},
167
+ > = Primitive<`Block:${TName}`, { type: TName; meta?: TMeta } & TFields>
168
+ export type Blocks<TBlock extends Block = Block> = Primitive<"Blocks", TBlock[]>
169
+
170
+ /**
171
+ * Nullable column (`T | null` in Postgres). **`Model`** flattens this to an optional property
172
+ * **`key?: T | null`** on the inferred row shape so literals and seeds omit `coverImage`-style keys
173
+ * without casts.
174
+ */
175
+ export type Optional<T> = Modifier<"Optional", T | null>
176
+ export type Unique<T> = Modifier<"Unique", T>
177
+ export type Indexed<T> = Modifier<"Indexed", T>
178
+ export type Searchable<T> = Modifier<"Searchable", T>
179
+ export type EditorReadOnly<T> = Modifier<"EditorReadOnly", T>
180
+ /**
181
+ * DB / trigger maintained only: Studio treats as read-only + server-generated on insert.
182
+ * There is **no** live preview or “follow title until edited” UX — declare dependencies with
183
+ * {@link ComputedFrom} instead if you want slug-like preview in Studio.
184
+ */
185
+ export type Computed<T> = Modifier<"Computed", T>
186
+ /**
187
+ * Plain-text column with Studio preview built from `sources` until the author edits the field
188
+ * on create (same UX as {@link Slug}). Database column is ordinary TEXT; optional overrides are persisted.
189
+ *
190
+ * Use `Optional<ComputedFrom<…>>` when the field may be null.
191
+ *
192
+ * **Second type argument — three shapes:**
193
+ * - **One field** (concat preview): `ComputedFrom<string, "title">`
194
+ * - **Several fields** (join with spaces, then truncate): `ComputedFrom<string, readonly ["title", "subtitle"]>`
195
+ * - **Template string** (placeholders + optional `truncate`): a string literal containing `{fieldName}` and/or
196
+ * `{truncate(fieldName, maxChars)}`. Dependencies are inferred for validation and Studio.
197
+ *
198
+ * Template examples (single string literal type; use real `\n` in the string when you want a newline):
199
+ * - `ComputedFrom<string, "Author: {authorProfile} | {created_at}">`
200
+ * - `ComputedFrom<string, "{truncate(body, 100)}">`
201
+ * - `ComputedFrom<string, "Author: {authorProfile} | Date: {created_at}\n{truncate(body, 100)}">`
202
+ */
203
+ export type ComputedFrom<
204
+ TValue,
205
+ TSources extends string | readonly string[] = "title",
206
+ > = Modifier<"ComputedFrom", TValue>
207
+ export type PrimaryKey<T> = Modifier<"PrimaryKey", T>
208
+ export type AutoIncrement<T extends number | bigint> = Modifier<"AutoIncrement", T>
209
+ /**
210
+ * Column value is assigned by the database (trigger, `DEFAULT`, sequence, etc.).
211
+ * Inserts may omit this field; generated client `Insert` types mark it optional.
212
+ */
213
+ export type ServerDefault<T> = Modifier<"ServerDefault", T>
214
+ export type Default<T, V> = Modifier<`Default:${Extract<V, string | number | boolean | bigint | null>}`, T>
215
+ export type MaxLength<T, N extends number> = Modifier<`MaxLength:${N}`, T>
216
+ export type MinLength<T, N extends number> = Modifier<`MinLength:${N}`, T>
217
+ export type Between<T, Min extends number, Max extends number> = Modifier<`Between:${Min}:${Max}`, T>
218
+ /**
219
+ * Built-in audit pair: expands to columns with DB `DEFAULT NOW()` and Studio prefill on create.
220
+ *
221
+ * Equivalent manual fields: naming columns `created_at` / `updated_at` plus `Timestamp` / `ServerDefault<DateTime>`
222
+ * wires the same defaults in the extractor; you don’t need this mixin unless you prefer the shorthand.
223
+ *
224
+ * Arbitrary timestamps use `ServerDefault<DateTime>` (or `@default`/`Expression` via engine fixtures) —
225
+ * those are configurable; only the **names** above get the convention treatment without extra wrappers.
226
+ */
227
+ export type Timestamps = {
228
+ created_at: ServerDefault<Date>
229
+ updated_at: ServerDefault<Date>
230
+ }
231
+ export type SoftDelete = {
232
+ deleted_at: Optional<Date>
233
+ }
234
+ export type Publishable = {
235
+ published_at: Optional<Date>
236
+ }
237
+ export type WithTimestamps<T extends Record<string, unknown>> = T & Timestamps
238
+ export type WithSoftDelete<T extends Record<string, unknown>> = T & SoftDelete
239
+ export type WithPublishable<T extends Record<string, unknown>> = T & Publishable
240
+
241
+ export type OnDelete = "cascade" | "setNull" | "restrict" | "noAction"
242
+ export type RelationOptions = {
243
+ required?: boolean
244
+ onDelete?: OnDelete
245
+ inverse?: string
246
+ }
247
+ export type RelatedTo<T, TOptions extends RelationOptions = {}> = Relation<"RelatedTo", T> & {
248
+ readonly __relationOptions?: TOptions
249
+ readonly __relationKind: "relatedTo"
250
+ }
251
+ export type HasMany<T, TOptions extends RelationOptions = {}> = Relation<"HasMany", T[]> & {
252
+ readonly __relationOptions?: TOptions
253
+ readonly __relationKind: "hasMany"
254
+ }
255
+ export type HasOne<T, TOptions extends RelationOptions = {}> = Relation<"HasOne", T | null> & {
256
+ readonly __relationOptions?: TOptions
257
+ readonly __relationKind: "hasOne"
258
+ }
259
+ export type ManyToMany<T, TOptions extends RelationOptions = {}> = Relation<"ManyToMany", T[]> & {
260
+ readonly __relationOptions?: TOptions
261
+ readonly __relationKind: "manyToMany"
262
+ }
263
+
264
+ export type Public = Access<"Public">
265
+ export type Private = Access<"Private">
266
+ export type LoggedIn = Access<"LoggedIn">
267
+ export type SupatypeAuthUser = Primitive<"SupatypeAuthUser", { readonly system: "supatype:user" }>
268
+ export type SupatypeAuthUserId = Primitive<"SupatypeAuthUserId", string>
269
+ type ModelFieldKeys<TModel> =
270
+ TModel extends { readonly [SUPATYPE_MODEL]?: { readonly fields: infer TFields } }
271
+ ? Extract<keyof TFields, string>
272
+ : never
273
+ type RelationFieldKeys<TFields extends Record<string, unknown>> = Extract<{
274
+ [K in keyof TFields]-?: K extends string
275
+ ? TFields[K] extends { readonly __relationKind: "relatedTo" }
276
+ ? K
277
+ : never
278
+ : never
279
+ }[keyof TFields], string>
280
+ type RelationOwnerKeys<TFields extends Record<string, unknown>> = Extract<{
281
+ [K in keyof TFields]-?: K extends string
282
+ ? TFields[K] extends { readonly __relationKind: "relatedTo" }
283
+ ? `${K}_id`
284
+ : never
285
+ : never
286
+ }[keyof TFields], string>
287
+ type SelfOwnerKey<TFields extends Record<string, unknown>> =
288
+ "id" extends keyof TFields
289
+ ? TFields["id"] extends SupatypeAuthUserId
290
+ ? "id"
291
+ : never
292
+ : never
293
+ type OwnerEligibleFieldKeys<TFields extends Record<string, unknown>> =
294
+ RelationOwnerKeys<TFields> | SelfOwnerKey<TFields>
295
+ type ModelRelationFieldKeys<TModel> =
296
+ TModel extends { readonly [SUPATYPE_MODEL]?: { readonly fields: infer TFields } }
297
+ ? TFields extends Record<string, unknown>
298
+ ? OwnerEligibleFieldKeys<TFields>
299
+ : never
300
+ : never
301
+ type AnySupatypeModel = {
302
+ readonly [SUPATYPE_MODEL]?: {
303
+ readonly fields: Record<string, unknown>
304
+ readonly meta: unknown
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Ownership access rule.
310
+ *
311
+ * Backward-compatible form:
312
+ * Owner<"author_id">
313
+ *
314
+ * Typed model form (preferred, autocomplete + validation):
315
+ * Owner<Post, "author_id">
316
+ */
317
+ export type Owner<
318
+ TModelOrKey extends string | AnySupatypeModel,
319
+ TKey extends TModelOrKey extends string ? string : ModelRelationFieldKeys<TModelOrKey> = TModelOrKey extends string
320
+ ? TModelOrKey
321
+ : never,
322
+ > = Access<"Owner", {
323
+ readonly kind: "Owner"
324
+ readonly key: TKey
325
+ readonly __ownerModel?: TModelOrKey extends string ? never : TModelOrKey
326
+ }>
327
+ export type OwnerKey<TModel extends AnySupatypeModel> =
328
+ ModelRelationFieldKeys<TModel>
329
+ export type OwnerOf<
330
+ TModel extends AnySupatypeModel,
331
+ TKey extends OwnerKey<TModel>,
332
+ > = Owner<TModel, TKey>
333
+ export type OwnerFrom<TRelationField extends string> = Access<"OwnerFrom", {
334
+ readonly kind: "OwnerFrom"
335
+ readonly relation: TRelationField
336
+ }>
337
+ export type Role<R extends string = string> = Access<"Role", { readonly kind: "Role"; readonly role: R }>
338
+ type BoundOwnerForFields<TFields extends Record<string, unknown>> = Access<"Owner", {
339
+ readonly kind: "Owner"
340
+ readonly key: OwnerEligibleFieldKeys<TFields>
341
+ readonly __ownerModel?: Model<TFields, any>
342
+ }>
343
+ type BoundOwnerFromForFields<TFields extends Record<string, unknown>> = Access<"OwnerFrom", {
344
+ readonly kind: "OwnerFrom"
345
+ readonly relation: RelationFieldKeys<TFields>
346
+ }>
347
+
348
+ /** `Optional<…>` wraps `Modifier<"Optional", …>` (detect structurally — do not inspect `[SUPATYPE_TYPE]`; primitives under `Optional` add their own tags and tag intersections are unreliable). */
349
+ type IsModifierOptional<V> = [V] extends [Modifier<"Optional", infer _>] ? true : false
350
+
351
+ type InferOptionalInner<V> = V extends Modifier<"Optional", infer Inner> ? Inner : never
352
+
353
+ type IsModifierLocalized<V> = [V] extends [Modifier<"Localized", infer _>] ? true : false
354
+
355
+ type InferLocalizedInner<V> = V extends Modifier<"Localized", infer Inner> ? Inner : never
356
+
357
+ type IsModifierNotLocalized<V> = [V] extends [Modifier<"NotLocalized", infer _>] ? true : false
358
+
359
+ type InferNotLocalizedInner<V> = V extends Modifier<"NotLocalized", infer Inner> ? Inner : never
360
+
361
+ type ImageAssetLocalizedOption<V> =
362
+ V extends ImageAsset<infer _B, infer O> ? (O extends { localized: true } ? true : false) : false
363
+
364
+ type FileAssetLocalizedOption<V> =
365
+ V extends FileAsset<infer _B, infer O> ? (O extends { localized: true } ? true : false) : false
366
+
367
+ /** Apply default localization to copy-like fields (used by {@link LocalizedModel}). */
368
+ type ApplyAutoLocalizedField<V> =
369
+ IsModifierOptional<V> extends true
370
+ ? Optional<ApplyAutoLocalizedField<InferOptionalInner<V>>>
371
+ : IsModifierNotLocalized<V> extends true
372
+ ? InferNotLocalizedInner<V>
373
+ : IsModifierLocalized<V> extends true
374
+ ? V
375
+ : V extends string
376
+ ? Localized<string>
377
+ : V extends RichText
378
+ ? Localized<RichText>
379
+ : V extends Markdown
380
+ ? Localized<Markdown>
381
+ : V extends Button
382
+ ? Localized<Button>
383
+ : ImageAssetLocalizedOption<V> extends true
384
+ ? Localized<V>
385
+ : FileAssetLocalizedOption<V> extends true
386
+ ? Localized<V>
387
+ : V
388
+
389
+ type ApplyAutoLocalizedFields<TFields extends Record<string, unknown>> = {
390
+ [K in keyof TFields]: ApplyAutoLocalizedField<TFields[K]>
391
+ }
392
+
393
+ /** Strip `Optional` / `Localized` / `NotLocalized` wrappers for inferred row shapes. */
394
+ type UnwrapModelFieldType<V> =
395
+ IsModifierOptional<V> extends true
396
+ ? UnwrapModelFieldType<InferOptionalInner<V>>
397
+ : IsModifierNotLocalized<V> extends true
398
+ ? UnwrapModelFieldType<InferNotLocalizedInner<V>>
399
+ : IsModifierLocalized<V> extends true
400
+ ? InferLocalizedInner<V>
401
+ : V
402
+
403
+ /** Row shape from `TFields`: `Optional<…>` → `key?: Inner` (`Inner` includes `null`). */
404
+ export type SpreadOptionalModelFields<TFields extends Record<string, unknown>> =
405
+ keyof TFields extends never
406
+ ? {}
407
+ : {
408
+ [K in keyof TFields as IsModifierOptional<TFields[K]> extends true ? never : K]: UnwrapModelFieldType<
409
+ TFields[K]
410
+ >
411
+ } & {
412
+ [K in keyof TFields as IsModifierOptional<TFields[K]> extends true
413
+ ? K extends keyof TFields & (string | number)
414
+ ? K
415
+ : never
416
+ : never]?: UnwrapModelFieldType<TFields[K]>
417
+ }
418
+
419
+ export type ModelMeta<TFields extends Record<string, unknown>> = {
420
+ access?: {
421
+ read?:
422
+ | Public
423
+ | Private
424
+ | LoggedIn
425
+ | Owner<OwnerEligibleFieldKeys<TFields>>
426
+ | OwnerFrom<RelationFieldKeys<TFields>>
427
+ | BoundOwnerForFields<TFields>
428
+ | BoundOwnerFromForFields<TFields>
429
+ | Role<string>
430
+ create?:
431
+ | Public
432
+ | Private
433
+ | LoggedIn
434
+ | Owner<OwnerEligibleFieldKeys<TFields>>
435
+ | OwnerFrom<RelationFieldKeys<TFields>>
436
+ | BoundOwnerForFields<TFields>
437
+ | BoundOwnerFromForFields<TFields>
438
+ | Role<string>
439
+ update?:
440
+ | Public
441
+ | Private
442
+ | LoggedIn
443
+ | Owner<OwnerEligibleFieldKeys<TFields>>
444
+ | OwnerFrom<RelationFieldKeys<TFields>>
445
+ | BoundOwnerForFields<TFields>
446
+ | BoundOwnerFromForFields<TFields>
447
+ | Role<string>
448
+ delete?:
449
+ | Public
450
+ | Private
451
+ | LoggedIn
452
+ | Owner<OwnerEligibleFieldKeys<TFields>>
453
+ | OwnerFrom<RelationFieldKeys<TFields>>
454
+ | BoundOwnerForFields<TFields>
455
+ | BoundOwnerFromForFields<TFields>
456
+ | Role<string>
457
+ }
458
+ tableName?: string
459
+ searchable?: readonly string[]
460
+ /** Exactly one row — Studio Globals, singleton partial unique index in Postgres. */
461
+ singleton?: true
462
+ /** When omitted, the CLI infers from `WithTimestamps` or `created_at` / `updated_at` fields. */
463
+ timestamps?: boolean
464
+ /** When omitted, the CLI infers from `WithSoftDelete` or `deleted_at`. */
465
+ softDelete?: boolean
466
+ /** When true, copy-like fields default to localized (same as {@link LocalizedModel}). */
467
+ autoLocalize?: true
468
+ }
469
+
470
+ /** Shorthand for singleton globals — `Model<Fields, GlobalMeta<Fields>>`. */
471
+ export type GlobalMeta<TFields extends Record<string, unknown>> = ModelMeta<TFields> & {
472
+ singleton: true
473
+ }
474
+
475
+ export type LocalizedModelMeta<TFields extends Record<string, unknown>> = ModelMeta<
476
+ ApplyAutoLocalizedFields<TFields>
477
+ > & {
478
+ autoLocalize?: true
479
+ }
480
+
481
+ export type Model<TFields extends Record<string, unknown>, TMeta extends ModelMeta<TFields> = {}> =
482
+ SpreadOptionalModelFields<TFields> & {
483
+ readonly [SUPATYPE_MODEL]?: {
484
+ readonly fields: TFields
485
+ readonly meta: TMeta
486
+ }
487
+ }
488
+
489
+ /**
490
+ * CMS-oriented model: plain `string` / `RichText` fields become localized automatically.
491
+ * Use {@link NotLocalized} to opt out; {@link ImageAsset}<Bucket, { localized: true }> to opt in for images.
492
+ */
493
+ export type LocalizedModel<
494
+ TFields extends Record<string, unknown>,
495
+ TMeta extends LocalizedModelMeta<TFields> = {},
496
+ > = Model<ApplyAutoLocalizedFields<TFields>, TMeta>
package/src/lexical.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface SerializedLexicalNode {
2
+ type: string
3
+ version: number
4
+ children?: SerializedLexicalNode[]
5
+ [key: string]: unknown
6
+ }
7
+
8
+ export interface SerializedEditorState {
9
+ root: SerializedLexicalNode
10
+ }
@@ -0,0 +1,72 @@
1
+ import { describe, expectTypeOf, it } from "vitest"
2
+ import type {
3
+ Bucket,
4
+ FileAsset,
5
+ ImageAsset,
6
+ Localized,
7
+ LocalizedModel,
8
+ Model,
9
+ NotLocalized,
10
+ Optional,
11
+ SpreadOptionalModelFields,
12
+ UUID,
13
+ } from "../src/index.js"
14
+
15
+ type Covers = Bucket<"post-covers", Record<string, unknown>>
16
+ type AttachmentBucket = Bucket<"attachments", Record<string, unknown>>
17
+
18
+ describe("SpreadOptionalModelFields", () => {
19
+ it("makes Optional<ImageAsset<bucket>> optional (modifiers stack on primitives)", () => {
20
+ type Fields = {
21
+ id: UUID
22
+ coverImage: Optional<ImageAsset<Covers>>
23
+ }
24
+ type Row = SpreadOptionalModelFields<Fields>
25
+
26
+ expectTypeOf<Row>().toMatchTypeOf<{ id: UUID; coverImage?: ImageAsset<Covers> | null }>()
27
+ })
28
+
29
+ it("Model<> row omits Optional ImageAsset columns", () => {
30
+ type M = Model<{
31
+ id: UUID
32
+ coverImage: Optional<ImageAsset<Covers>>
33
+ }>
34
+
35
+ const row: M = {
36
+ id: "" as UUID,
37
+ }
38
+ expectTypeOf(row).toMatchTypeOf<M>()
39
+ })
40
+
41
+ it("optional attachment FileAsset resolves to omitted keys on literals", () => {
42
+ type Row = SpreadOptionalModelFields<{
43
+ attachment: Optional<FileAsset<AttachmentBucket>>
44
+ }>
45
+ const _: Row = {}
46
+ expectTypeOf(_.attachment).toEqualTypeOf<FileAsset<AttachmentBucket> | null | undefined>()
47
+ })
48
+
49
+ it("unwraps Localized fields to locale-keyed records", () => {
50
+ type Row = SpreadOptionalModelFields<{
51
+ title: Localized<string>
52
+ subtitle: Optional<Localized<string>>
53
+ }>
54
+
55
+ expectTypeOf<Row>().toMatchTypeOf<{
56
+ title: Record<string, string>
57
+ subtitle?: Record<string, string> | null
58
+ }>()
59
+ })
60
+
61
+ it("LocalizedModel infers localized copy fields", () => {
62
+ type Row = LocalizedModel<{
63
+ hero_title: string
64
+ map_url: NotLocalized<string>
65
+ }>
66
+
67
+ expectTypeOf<Row>().toMatchTypeOf<{
68
+ hero_title: Record<string, string>
69
+ map_url: string
70
+ }>()
71
+ })
72
+ })
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=primitives.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.test.d.ts","sourceRoot":"","sources":["primitives.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,17 @@
1
+ import { describe, expectTypeOf, it } from "vitest";
2
+ describe("@supatype/types primitives", () => {
3
+ it("exposes branded field types with structural use-site compatibility", () => {
4
+ expectTypeOf().toMatchTypeOf();
5
+ expectTypeOf().toMatchTypeOf();
6
+ expectTypeOf().toMatchTypeOf();
7
+ });
8
+ it("supports relation wrappers and optional modifier composition", () => {
9
+ expectTypeOf().toEqualTypeOf();
10
+ expectTypeOf().toEqualTypeOf();
11
+ });
12
+ it("preserves model metadata markers for extractor discovery", () => {
13
+ expectTypeOf().toHaveProperty("id");
14
+ expectTypeOf().toHaveProperty("body");
15
+ });
16
+ });
17
+ //# sourceMappingURL=primitives.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.test.js","sourceRoot":"","sources":["primitives.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAqCnD,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,YAAY,EAAc,CAAC,aAAa,EAAU,CAAA;QAClD,YAAY,EAAgB,CAAC,aAAa,EAAU,CAAA;QACpD,YAAY,EAAoB,CAAC,aAAa,EAAsB,CAAA;IACtE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,YAAY,EAAoB,CAAC,aAAa,EAAa,CAAA;QAC3D,YAAY,EAAwB,CAAC,aAAa,EAAe,CAAA;IACnE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,YAAY,EAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;QACzC,YAAY,EAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}