shapedef 1.0.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/.gitignore +232 -0
- package/CLAUDE.md +41 -0
- package/README.md +2 -0
- package/dist/cjs/common/guards.d.ts +5 -0
- package/dist/cjs/common/guards.d.ts.map +1 -0
- package/dist/cjs/common/guards.js +22 -0
- package/dist/cjs/common/guards.js.map +1 -0
- package/dist/cjs/common/index.d.ts +3 -0
- package/dist/cjs/common/index.d.ts.map +1 -0
- package/dist/cjs/common/index.js +19 -0
- package/dist/cjs/common/index.js.map +1 -0
- package/dist/cjs/common/padding.d.ts +2 -0
- package/dist/cjs/common/padding.d.ts.map +1 -0
- package/dist/cjs/common/padding.js +8 -0
- package/dist/cjs/common/padding.js.map +1 -0
- package/dist/cjs/common/types.d.ts +8 -0
- package/dist/cjs/common/types.d.ts.map +1 -0
- package/dist/cjs/common/types.js +3 -0
- package/dist/cjs/common/types.js.map +1 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +21 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/inputs/index.d.ts +2 -0
- package/dist/cjs/inputs/index.d.ts.map +1 -0
- package/dist/cjs/inputs/index.js +18 -0
- package/dist/cjs/inputs/index.js.map +1 -0
- package/dist/cjs/inputs/json-schema.d.ts +4 -0
- package/dist/cjs/inputs/json-schema.d.ts.map +1 -0
- package/dist/cjs/inputs/json-schema.js +306 -0
- package/dist/cjs/inputs/json-schema.js.map +1 -0
- package/dist/cjs/phantom.d.ts +4 -0
- package/dist/cjs/phantom.d.ts.map +1 -0
- package/dist/cjs/phantom.js +5 -0
- package/dist/cjs/phantom.js.map +1 -0
- package/dist/cjs/shape-utils.d.ts +4 -0
- package/dist/cjs/shape-utils.d.ts.map +1 -0
- package/dist/cjs/shape-utils.js +43 -0
- package/dist/cjs/shape-utils.js.map +1 -0
- package/dist/cjs/shape.d.ts +169 -0
- package/dist/cjs/shape.d.ts.map +1 -0
- package/dist/cjs/shape.js +258 -0
- package/dist/cjs/shape.js.map +1 -0
- package/dist/cjs/translations/index.d.ts +4 -0
- package/dist/cjs/translations/index.d.ts.map +1 -0
- package/dist/cjs/translations/index.js +20 -0
- package/dist/cjs/translations/index.js.map +1 -0
- package/dist/cjs/translations/postgres.d.ts +3 -0
- package/dist/cjs/translations/postgres.d.ts.map +1 -0
- package/dist/cjs/translations/postgres.js +72 -0
- package/dist/cjs/translations/postgres.js.map +1 -0
- package/dist/cjs/translations/translation.d.ts +3 -0
- package/dist/cjs/translations/translation.d.ts.map +1 -0
- package/dist/cjs/translations/translation.js +3 -0
- package/dist/cjs/translations/translation.js.map +1 -0
- package/dist/cjs/translations/typescript.d.ts +3 -0
- package/dist/cjs/translations/typescript.d.ts.map +1 -0
- package/dist/cjs/translations/typescript.js +122 -0
- package/dist/cjs/translations/typescript.js.map +1 -0
- package/dist/cjs/validation.d.ts +10 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/cjs/validation.js +101 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/esm/common/guards.d.ts +5 -0
- package/dist/esm/common/guards.d.ts.map +1 -0
- package/dist/esm/common/guards.js +22 -0
- package/dist/esm/common/guards.js.map +1 -0
- package/dist/esm/common/index.d.ts +3 -0
- package/dist/esm/common/index.d.ts.map +1 -0
- package/dist/esm/common/index.js +19 -0
- package/dist/esm/common/index.js.map +1 -0
- package/dist/esm/common/padding.d.ts +2 -0
- package/dist/esm/common/padding.d.ts.map +1 -0
- package/dist/esm/common/padding.js +8 -0
- package/dist/esm/common/padding.js.map +1 -0
- package/dist/esm/common/types.d.ts +8 -0
- package/dist/esm/common/types.d.ts.map +1 -0
- package/dist/esm/common/types.js +3 -0
- package/dist/esm/common/types.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/inputs/index.d.ts +2 -0
- package/dist/esm/inputs/index.d.ts.map +1 -0
- package/dist/esm/inputs/index.js +18 -0
- package/dist/esm/inputs/index.js.map +1 -0
- package/dist/esm/inputs/json-schema.d.ts +4 -0
- package/dist/esm/inputs/json-schema.d.ts.map +1 -0
- package/dist/esm/inputs/json-schema.js +306 -0
- package/dist/esm/inputs/json-schema.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/phantom.d.ts +4 -0
- package/dist/esm/phantom.d.ts.map +1 -0
- package/dist/esm/phantom.js +5 -0
- package/dist/esm/phantom.js.map +1 -0
- package/dist/esm/shape-utils.d.ts +4 -0
- package/dist/esm/shape-utils.d.ts.map +1 -0
- package/dist/esm/shape-utils.js +43 -0
- package/dist/esm/shape-utils.js.map +1 -0
- package/dist/esm/shape.d.ts +169 -0
- package/dist/esm/shape.d.ts.map +1 -0
- package/dist/esm/shape.js +258 -0
- package/dist/esm/shape.js.map +1 -0
- package/dist/esm/translations/index.d.ts +4 -0
- package/dist/esm/translations/index.d.ts.map +1 -0
- package/dist/esm/translations/index.js +20 -0
- package/dist/esm/translations/index.js.map +1 -0
- package/dist/esm/translations/postgres.d.ts +3 -0
- package/dist/esm/translations/postgres.d.ts.map +1 -0
- package/dist/esm/translations/postgres.js +72 -0
- package/dist/esm/translations/postgres.js.map +1 -0
- package/dist/esm/translations/translation.d.ts +3 -0
- package/dist/esm/translations/translation.d.ts.map +1 -0
- package/dist/esm/translations/translation.js +3 -0
- package/dist/esm/translations/translation.js.map +1 -0
- package/dist/esm/translations/typescript.d.ts +3 -0
- package/dist/esm/translations/typescript.d.ts.map +1 -0
- package/dist/esm/translations/typescript.js +122 -0
- package/dist/esm/translations/typescript.js.map +1 -0
- package/dist/esm/validation.d.ts +10 -0
- package/dist/esm/validation.d.ts.map +1 -0
- package/dist/esm/validation.js +101 -0
- package/dist/esm/validation.js.map +1 -0
- package/package.json +44 -0
- package/pnpm-lock.yaml +969 -0
- package/src/common/guards.ts +23 -0
- package/src/common/index.ts +2 -0
- package/src/common/padding.ts +6 -0
- package/src/common/types.ts +21 -0
- package/src/index.ts +4 -0
- package/src/inputs/index.ts +1 -0
- package/src/inputs/json-schema.test.ts +191 -0
- package/src/inputs/json-schema.ts +324 -0
- package/src/phantom.ts +3 -0
- package/src/samples/bank.sample.ts +25 -0
- package/src/shape-utils.ts +46 -0
- package/src/shape.ts +488 -0
- package/src/translations/index.ts +3 -0
- package/src/translations/postgres.test.ts +161 -0
- package/src/translations/postgres.ts +73 -0
- package/src/translations/translation.ts +3 -0
- package/src/translations/typescript.test.ts +61 -0
- package/src/translations/typescript.ts +142 -0
- package/src/validation.test.ts +179 -0
- package/src/validation.ts +135 -0
- package/tsconfig.cjs.json +10 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +22 -0
package/src/shape.ts
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { has, isPlainObject } from "./common";
|
|
2
|
+
import { Prettify, TupleOf } from "./common/types";
|
|
3
|
+
import { Phantom, PhantomIgnore } from "./phantom";
|
|
4
|
+
|
|
5
|
+
export const ShapeBrandSymbol = Symbol("Shape");
|
|
6
|
+
export type ShapeBrandSymbol = typeof ShapeBrandSymbol;
|
|
7
|
+
|
|
8
|
+
export type ShapeAnnotation = {
|
|
9
|
+
name?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
optional?: boolean;
|
|
13
|
+
primary?: boolean;
|
|
14
|
+
unique?: boolean;
|
|
15
|
+
min?: number;
|
|
16
|
+
max?: number;
|
|
17
|
+
format?: string;
|
|
18
|
+
pattern?: string;
|
|
19
|
+
uniqueWithin?: boolean;
|
|
20
|
+
defaultValue?: any;
|
|
21
|
+
foreign?: {
|
|
22
|
+
shapeName: string;
|
|
23
|
+
fieldName: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type PhantomShape = {
|
|
28
|
+
out: any;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type LooseShape = {
|
|
32
|
+
type: string;
|
|
33
|
+
__phantom: PhantomShape;
|
|
34
|
+
anno: ShapeAnnotation;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type OutputOfShape<T> =
|
|
38
|
+
[T] extends [LooseShape] ? T["__phantom"]["out"] : never;
|
|
39
|
+
|
|
40
|
+
export type AnnoOfShape<T> = [T] extends [LooseShape] ? T["anno"] : never;
|
|
41
|
+
|
|
42
|
+
export type ShapeIsOptional<T> =
|
|
43
|
+
[AnnoOfShape<T>["optional"]] extends [true] ? true : false;
|
|
44
|
+
|
|
45
|
+
export type AnnotateShape<
|
|
46
|
+
T extends LooseShape,
|
|
47
|
+
Anno extends ShapeAnnotation,
|
|
48
|
+
> = T & { anno: T["anno"] & Anno };
|
|
49
|
+
|
|
50
|
+
export interface ShapeBase<Type extends string> extends LooseShape {
|
|
51
|
+
type: Type;
|
|
52
|
+
[ShapeBrandSymbol]: true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ShapeWithOutput<
|
|
56
|
+
Type extends string,
|
|
57
|
+
Out,
|
|
58
|
+
> extends ShapeBase<Type> {
|
|
59
|
+
__phantom: { out: Out };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ShapeFloat extends ShapeWithOutput<"float", number> {}
|
|
63
|
+
export interface ShapeInt extends ShapeWithOutput<"int", number> {}
|
|
64
|
+
export interface ShapeNumber extends ShapeWithOutput<"number", number> {}
|
|
65
|
+
export interface ShapeBool extends ShapeWithOutput<"bool", boolean> {}
|
|
66
|
+
export interface ShapeStr extends ShapeWithOutput<"str", string> {}
|
|
67
|
+
export interface ShapeDate extends ShapeWithOutput<"date", Date> {}
|
|
68
|
+
export interface ShapeNil extends ShapeWithOutput<"nil", null> {}
|
|
69
|
+
export interface ShapeUnknown extends ShapeWithOutput<"unknown", unknown> {}
|
|
70
|
+
|
|
71
|
+
export interface ShapeLiteralFloat<
|
|
72
|
+
Value extends number = number,
|
|
73
|
+
> extends ShapeWithOutput<"literal-float", Value> {
|
|
74
|
+
value: Value;
|
|
75
|
+
}
|
|
76
|
+
export interface ShapeLiteralInt<
|
|
77
|
+
Value extends number = number,
|
|
78
|
+
> extends ShapeWithOutput<"literal-int", Value> {
|
|
79
|
+
value: Value;
|
|
80
|
+
}
|
|
81
|
+
export interface ShapeLiteralNumber<
|
|
82
|
+
Value extends number = number,
|
|
83
|
+
> extends ShapeWithOutput<"literal-number", Value> {
|
|
84
|
+
value: Value;
|
|
85
|
+
}
|
|
86
|
+
export interface ShapeLiteralBool<
|
|
87
|
+
Value extends boolean = boolean,
|
|
88
|
+
> extends ShapeWithOutput<"literal-bool", Value> {
|
|
89
|
+
value: Value;
|
|
90
|
+
}
|
|
91
|
+
export interface ShapeLiteralStr<
|
|
92
|
+
Value extends string = string,
|
|
93
|
+
> extends ShapeWithOutput<"literal-str", Value> {
|
|
94
|
+
value: Value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ShapeRef<
|
|
98
|
+
Name extends string = string,
|
|
99
|
+
Hint extends Shape = ShapeUnknown,
|
|
100
|
+
> extends ShapeBase<"ref"> {
|
|
101
|
+
name: Name;
|
|
102
|
+
hint?: Hint;
|
|
103
|
+
__phantom: { out: unknown };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ShapeArray<
|
|
107
|
+
Item extends Shape = Shape,
|
|
108
|
+
> extends ShapeBase<"array"> {
|
|
109
|
+
item: Item;
|
|
110
|
+
__phantom: { out: Array<OutputOfShape<Item>> };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ShapeTuple<
|
|
114
|
+
out Items extends TupleOf<Shape> = TupleOf<Shape>,
|
|
115
|
+
> extends ShapeBase<"tuple"> {
|
|
116
|
+
tup: Items;
|
|
117
|
+
__phantom: { out: { -readonly [P in keyof Items]: Items[P] } };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ShapeMapping<
|
|
121
|
+
Rec extends Record<string, Shape> = Record<string, Shape>,
|
|
122
|
+
> extends ShapeBase<"mapping"> {
|
|
123
|
+
rec: Rec;
|
|
124
|
+
__phantom: {
|
|
125
|
+
out: Prettify<
|
|
126
|
+
{
|
|
127
|
+
-readonly [P in keyof Rec as [ShapeIsOptional<Rec[P]>] extends [true] ?
|
|
128
|
+
never
|
|
129
|
+
: P]: OutputOfShape<Rec[P]>;
|
|
130
|
+
} & {
|
|
131
|
+
-readonly [P in keyof Rec as [ShapeIsOptional<Rec[P]>] extends [true] ?
|
|
132
|
+
P
|
|
133
|
+
: never]?: OutputOfShape<Rec[P]>;
|
|
134
|
+
}
|
|
135
|
+
>;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ShapeVector<
|
|
140
|
+
Dims extends number = number,
|
|
141
|
+
> extends ShapeWithOutput<"vector", number[]> {
|
|
142
|
+
dims: Dims;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface ShapeUnion<
|
|
146
|
+
out Ofs extends TupleOf<Shape> | Array<Shape> = TupleOf<Shape> | Array<Shape>,
|
|
147
|
+
> extends ShapeWithOutput<"union", OutputOfShape<Ofs[number]>> {
|
|
148
|
+
ofs: Ofs;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type Shape =
|
|
152
|
+
| ShapeFloat
|
|
153
|
+
| ShapeInt
|
|
154
|
+
| ShapeNumber
|
|
155
|
+
| ShapeBool
|
|
156
|
+
| ShapeStr
|
|
157
|
+
| ShapeDate
|
|
158
|
+
| ShapeNil
|
|
159
|
+
| ShapeUnknown
|
|
160
|
+
| ShapeLiteralFloat
|
|
161
|
+
| ShapeLiteralInt
|
|
162
|
+
| ShapeLiteralNumber
|
|
163
|
+
| ShapeLiteralBool
|
|
164
|
+
| ShapeLiteralStr
|
|
165
|
+
| ShapeRef
|
|
166
|
+
| ShapeArray
|
|
167
|
+
| ShapeMapping
|
|
168
|
+
| ShapeTuple
|
|
169
|
+
| ShapeVector
|
|
170
|
+
| ShapeUnion;
|
|
171
|
+
|
|
172
|
+
const createPhantom = (): {
|
|
173
|
+
__phantom: { out: PhantomIgnore };
|
|
174
|
+
[ShapeBrandSymbol]: true;
|
|
175
|
+
} => {
|
|
176
|
+
return { __phantom: { out: Phantom }, [ShapeBrandSymbol]: true };
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export function isShape(x: unknown): x is Shape {
|
|
180
|
+
return (
|
|
181
|
+
has(x, ShapeBrandSymbol) &&
|
|
182
|
+
(x as { [ShapeBrandSymbol]: unknown })[ShapeBrandSymbol] === true
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export namespace shapes {
|
|
187
|
+
export function float(): ShapeFloat {
|
|
188
|
+
return {
|
|
189
|
+
type: "float",
|
|
190
|
+
anno: {},
|
|
191
|
+
...createPhantom(),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function int(): ShapeInt {
|
|
196
|
+
return {
|
|
197
|
+
type: "int",
|
|
198
|
+
anno: {},
|
|
199
|
+
...createPhantom(),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function number(): ShapeNumber {
|
|
204
|
+
return {
|
|
205
|
+
type: "number",
|
|
206
|
+
anno: {},
|
|
207
|
+
...createPhantom(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function bool(): ShapeBool {
|
|
212
|
+
return {
|
|
213
|
+
type: "bool",
|
|
214
|
+
anno: {},
|
|
215
|
+
...createPhantom(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function str(): ShapeStr {
|
|
220
|
+
return {
|
|
221
|
+
type: "str",
|
|
222
|
+
anno: {},
|
|
223
|
+
...createPhantom(),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function date(): ShapeDate {
|
|
228
|
+
return {
|
|
229
|
+
type: "date",
|
|
230
|
+
anno: {},
|
|
231
|
+
...createPhantom(),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function nil(): ShapeNil {
|
|
236
|
+
return {
|
|
237
|
+
type: "nil",
|
|
238
|
+
anno: {},
|
|
239
|
+
...createPhantom(),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function unknown(): ShapeUnknown {
|
|
244
|
+
return {
|
|
245
|
+
type: "unknown",
|
|
246
|
+
anno: {},
|
|
247
|
+
...createPhantom(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function literalFloat<Value extends number>(
|
|
252
|
+
value: Value,
|
|
253
|
+
): ShapeLiteralFloat<Value> {
|
|
254
|
+
return {
|
|
255
|
+
type: "literal-float",
|
|
256
|
+
value: value,
|
|
257
|
+
anno: {},
|
|
258
|
+
...createPhantom(),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function literalInt<Value extends number>(
|
|
263
|
+
value: Value,
|
|
264
|
+
): ShapeLiteralInt<Value> {
|
|
265
|
+
return {
|
|
266
|
+
type: "literal-int",
|
|
267
|
+
value: value,
|
|
268
|
+
anno: {},
|
|
269
|
+
...createPhantom(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function literalNumber<Value extends number>(
|
|
274
|
+
value: Value,
|
|
275
|
+
): ShapeLiteralNumber<Value> {
|
|
276
|
+
return {
|
|
277
|
+
type: "literal-number",
|
|
278
|
+
value: value,
|
|
279
|
+
anno: {},
|
|
280
|
+
...createPhantom(),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function literalBool<Value extends boolean>(
|
|
285
|
+
value: Value,
|
|
286
|
+
): ShapeLiteralBool<Value> {
|
|
287
|
+
return {
|
|
288
|
+
type: "literal-bool",
|
|
289
|
+
value: value,
|
|
290
|
+
anno: {},
|
|
291
|
+
...createPhantom(),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function literalStr<Value extends string>(
|
|
296
|
+
value: Value,
|
|
297
|
+
): ShapeLiteralStr<Value> {
|
|
298
|
+
return {
|
|
299
|
+
type: "literal-str",
|
|
300
|
+
value: value,
|
|
301
|
+
anno: {},
|
|
302
|
+
...createPhantom(),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function literal<Value extends number>(
|
|
307
|
+
value: Value,
|
|
308
|
+
): ShapeLiteralNumber<Value>;
|
|
309
|
+
export function literal<Value extends boolean>(
|
|
310
|
+
value: Value,
|
|
311
|
+
): ShapeLiteralBool<Value>;
|
|
312
|
+
export function literal<Value extends string>(
|
|
313
|
+
value: Value,
|
|
314
|
+
): ShapeLiteralStr<Value>;
|
|
315
|
+
export function literal(
|
|
316
|
+
value: number | boolean | string,
|
|
317
|
+
): ShapeLiteralNumber | ShapeLiteralBool | ShapeLiteralStr;
|
|
318
|
+
export function literal(
|
|
319
|
+
value: number | boolean | string,
|
|
320
|
+
): ShapeLiteralNumber | ShapeLiteralBool | ShapeLiteralStr {
|
|
321
|
+
if (typeof value === "number") return literalNumber(value);
|
|
322
|
+
if (typeof value === "boolean") return literalBool(value);
|
|
323
|
+
return literalStr(value);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function ref<Name extends string, Hint extends Shape = ShapeUnknown>(
|
|
327
|
+
name: Name,
|
|
328
|
+
hint?: Hint,
|
|
329
|
+
): ShapeRef<Name, Hint> {
|
|
330
|
+
return {
|
|
331
|
+
type: "ref",
|
|
332
|
+
name: name,
|
|
333
|
+
anno: {},
|
|
334
|
+
...(hint ?
|
|
335
|
+
{
|
|
336
|
+
hint: hint,
|
|
337
|
+
}
|
|
338
|
+
: {}),
|
|
339
|
+
...createPhantom(),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function array<Item extends Shape>(item: Item): ShapeArray<Item> {
|
|
344
|
+
return {
|
|
345
|
+
type: "array",
|
|
346
|
+
item: item,
|
|
347
|
+
anno: {},
|
|
348
|
+
...createPhantom(),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function tuple<Items extends TupleOf<Shape>>(
|
|
353
|
+
...items: Items
|
|
354
|
+
): ShapeTuple<Items> {
|
|
355
|
+
return {
|
|
356
|
+
type: "tuple",
|
|
357
|
+
tup: items,
|
|
358
|
+
anno: {},
|
|
359
|
+
...createPhantom(),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function mapping<Rec extends Record<string, Shape>>(
|
|
364
|
+
rec: Rec,
|
|
365
|
+
): ShapeMapping<Rec> {
|
|
366
|
+
return {
|
|
367
|
+
type: "mapping",
|
|
368
|
+
rec: rec,
|
|
369
|
+
anno: {},
|
|
370
|
+
...createPhantom(),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function vector<Dims extends number>(dims: Dims): ShapeVector<Dims> {
|
|
375
|
+
return {
|
|
376
|
+
type: "vector",
|
|
377
|
+
dims: dims,
|
|
378
|
+
anno: {},
|
|
379
|
+
...createPhantom(),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function union<Ofs extends Array<Shape>>(...ofs: Ofs): ShapeUnion<Ofs>;
|
|
384
|
+
export function union<Ofs extends TupleOf<Shape>>(
|
|
385
|
+
...ofs: Ofs
|
|
386
|
+
): ShapeUnion<Ofs>;
|
|
387
|
+
export function union<Ofs extends TupleOf<Shape>>(
|
|
388
|
+
...ofs: Ofs
|
|
389
|
+
): ShapeUnion<Ofs> {
|
|
390
|
+
return {
|
|
391
|
+
type: "union",
|
|
392
|
+
ofs: ofs,
|
|
393
|
+
anno: {},
|
|
394
|
+
...createPhantom(),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export namespace annotate {
|
|
400
|
+
export function by<S extends Shape, Anno extends ShapeAnnotation>(
|
|
401
|
+
shape: S,
|
|
402
|
+
anno: Anno,
|
|
403
|
+
): AnnotateShape<S, Anno> {
|
|
404
|
+
return {
|
|
405
|
+
...shape,
|
|
406
|
+
anno: {
|
|
407
|
+
...anno,
|
|
408
|
+
...shape.anno,
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function optional<S extends Shape>(
|
|
414
|
+
shape: S,
|
|
415
|
+
): AnnotateShape<S, { optional: true }> {
|
|
416
|
+
return by(shape, { optional: true });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function primary<S extends Shape>(
|
|
420
|
+
shape: S,
|
|
421
|
+
): AnnotateShape<S, { primary: true }> {
|
|
422
|
+
return by(shape, { primary: true });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function unique<S extends Shape>(
|
|
426
|
+
shape: S,
|
|
427
|
+
): AnnotateShape<S, { unique: true }> {
|
|
428
|
+
return by(shape, { unique: true });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function named<S extends Shape, Name extends string>(
|
|
432
|
+
shape: S,
|
|
433
|
+
name: Name,
|
|
434
|
+
): AnnotateShape<S, { name: Name }> {
|
|
435
|
+
return by(shape, { name: name });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function foreign<
|
|
439
|
+
S extends Shape,
|
|
440
|
+
ShapeName extends string,
|
|
441
|
+
FieldName extends string,
|
|
442
|
+
>(
|
|
443
|
+
shape: S,
|
|
444
|
+
shapeName: ShapeName,
|
|
445
|
+
fieldName: FieldName,
|
|
446
|
+
): AnnotateShape<
|
|
447
|
+
S,
|
|
448
|
+
{ foreign: { shapeName: ShapeName; fieldName: FieldName } }
|
|
449
|
+
> {
|
|
450
|
+
return by(shape, { foreign: { shapeName, fieldName } });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function auto(
|
|
454
|
+
x: unknown,
|
|
455
|
+
options: {
|
|
456
|
+
preferLiterals?: boolean;
|
|
457
|
+
} = {},
|
|
458
|
+
): Shape {
|
|
459
|
+
if (isShape(x)) return x;
|
|
460
|
+
if (x === null) return shapes.nil();
|
|
461
|
+
if (typeof x === "undefined") return shapes.unknown();
|
|
462
|
+
if (typeof x === "number")
|
|
463
|
+
return options.preferLiterals ? shapes.literalNumber(x) : shapes.number();
|
|
464
|
+
if (typeof x === "string")
|
|
465
|
+
return options.preferLiterals ? shapes.literalStr(x) : shapes.str();
|
|
466
|
+
if (typeof x === "boolean")
|
|
467
|
+
return options.preferLiterals ? shapes.literalBool(x) : shapes.bool();
|
|
468
|
+
if (x instanceof Date) return shapes.date();
|
|
469
|
+
if (Array.isArray(x)) {
|
|
470
|
+
if (x.length <= 0) return shapes.array(shapes.unknown());
|
|
471
|
+
if (x.length === 1) return shapes.array(auto(x[0], options));
|
|
472
|
+
const types = new Set(x.map((v) => typeof v));
|
|
473
|
+
return shapes.array(
|
|
474
|
+
types.size === 1 ?
|
|
475
|
+
auto(x[0], options)
|
|
476
|
+
: shapes.union(...x.map((v) => auto(v, options))),
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
if (isPlainObject(x)) {
|
|
480
|
+
return shapes.mapping(
|
|
481
|
+
Object.fromEntries(
|
|
482
|
+
Object.entries(x).map(([k, v]) => [k, auto(v, options)]),
|
|
483
|
+
),
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
return shapes.unknown();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, expect } from "vitest";
|
|
2
|
+
import { annotate, shapes } from "../shape";
|
|
3
|
+
import { postgres } from "./postgres";
|
|
4
|
+
|
|
5
|
+
describe("postgres", (it) => {
|
|
6
|
+
it("translates primitive types", () => {
|
|
7
|
+
expect(postgres(shapes.int())).toBe("INTEGER");
|
|
8
|
+
expect(postgres(shapes.float())).toBe("DOUBLE PRECISION");
|
|
9
|
+
expect(postgres(shapes.number())).toBe("NUMERIC");
|
|
10
|
+
expect(postgres(shapes.bool())).toBe("BOOLEAN");
|
|
11
|
+
expect(postgres(shapes.str())).toBe("TEXT");
|
|
12
|
+
expect(postgres(shapes.date())).toBe("TIMESTAMPTZ");
|
|
13
|
+
expect(postgres(shapes.nil())).toBe("NULL");
|
|
14
|
+
expect(postgres(shapes.unknown())).toBe("JSONB");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("translates vector type (pgvector)", () => {
|
|
18
|
+
expect(postgres(shapes.vector(300))).toBe("vector(300)");
|
|
19
|
+
expect(postgres(shapes.vector(1536))).toBe("vector(1536)");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("translates arrays of primitives to native PG array types", () => {
|
|
23
|
+
expect(postgres(shapes.array(shapes.str()))).toBe("TEXT[]");
|
|
24
|
+
expect(postgres(shapes.array(shapes.int()))).toBe("INTEGER[]");
|
|
25
|
+
expect(postgres(shapes.array(shapes.float()))).toBe("DOUBLE PRECISION[]");
|
|
26
|
+
expect(postgres(shapes.array(shapes.number()))).toBe("NUMERIC[]");
|
|
27
|
+
expect(postgres(shapes.array(shapes.bool()))).toBe("BOOLEAN[]");
|
|
28
|
+
expect(postgres(shapes.array(shapes.date()))).toBe("TIMESTAMPTZ[]");
|
|
29
|
+
expect(postgres(shapes.array(shapes.vector(3)))).toBe("vector(3)[]");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("translates multi-dimensional arrays", () => {
|
|
33
|
+
expect(postgres(shapes.array(shapes.array(shapes.int())))).toBe(
|
|
34
|
+
"INTEGER[][]",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("falls back to JSONB for arrays of complex types", () => {
|
|
39
|
+
expect(
|
|
40
|
+
postgres(shapes.array(shapes.union(shapes.int(), shapes.str()))),
|
|
41
|
+
).toBe("JSONB");
|
|
42
|
+
expect(postgres(shapes.array(shapes.mapping({ x: shapes.int() })))).toBe(
|
|
43
|
+
"JSONB",
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("translates other composite types to JSONB", () => {
|
|
48
|
+
expect(postgres(shapes.tuple(shapes.int(), shapes.str()))).toBe("JSONB");
|
|
49
|
+
expect(postgres(shapes.union(shapes.int(), shapes.str()))).toBe("JSONB");
|
|
50
|
+
expect(postgres(shapes.mapping({ x: shapes.int() }))).toBe("JSONB");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("generates CREATE TABLE IF NOT EXISTS for named mappings", () => {
|
|
54
|
+
expect(
|
|
55
|
+
postgres(
|
|
56
|
+
annotate.named(
|
|
57
|
+
shapes.mapping({
|
|
58
|
+
id: annotate.primary(shapes.int()),
|
|
59
|
+
name: shapes.str(),
|
|
60
|
+
}),
|
|
61
|
+
"User",
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
).toBe(
|
|
65
|
+
[
|
|
66
|
+
"CREATE TABLE IF NOT EXISTS User (",
|
|
67
|
+
" id INTEGER PRIMARY KEY,",
|
|
68
|
+
" name TEXT NOT NULL",
|
|
69
|
+
")",
|
|
70
|
+
].join("\n"),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("generates CREATE TABLE IF NOT EXISTS with vector column", () => {
|
|
75
|
+
expect(
|
|
76
|
+
postgres(
|
|
77
|
+
annotate.named(
|
|
78
|
+
shapes.mapping({
|
|
79
|
+
id: annotate.primary(shapes.int()),
|
|
80
|
+
my_embedding: shapes.vector(300),
|
|
81
|
+
}),
|
|
82
|
+
"Foobar",
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
).toBe(
|
|
86
|
+
[
|
|
87
|
+
"CREATE TABLE IF NOT EXISTS Foobar (",
|
|
88
|
+
" id INTEGER PRIMARY KEY,",
|
|
89
|
+
" my_embedding vector(300) NOT NULL",
|
|
90
|
+
")",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("marks optional columns as nullable (no NOT NULL)", () => {
|
|
96
|
+
expect(
|
|
97
|
+
postgres(
|
|
98
|
+
annotate.named(
|
|
99
|
+
shapes.mapping({
|
|
100
|
+
id: annotate.primary(shapes.int()),
|
|
101
|
+
nickname: annotate.optional(shapes.str()),
|
|
102
|
+
}),
|
|
103
|
+
"Profile",
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
).toBe(
|
|
107
|
+
[
|
|
108
|
+
"CREATE TABLE IF NOT EXISTS Profile (",
|
|
109
|
+
" id INTEGER PRIMARY KEY,",
|
|
110
|
+
" nickname TEXT",
|
|
111
|
+
")",
|
|
112
|
+
].join("\n"),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("adds UNIQUE constraint", () => {
|
|
117
|
+
expect(
|
|
118
|
+
postgres(
|
|
119
|
+
annotate.named(
|
|
120
|
+
shapes.mapping({
|
|
121
|
+
id: annotate.primary(shapes.int()),
|
|
122
|
+
email: annotate.unique(shapes.str()),
|
|
123
|
+
}),
|
|
124
|
+
"Account",
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
).toBe(
|
|
128
|
+
[
|
|
129
|
+
"CREATE TABLE IF NOT EXISTS Account (",
|
|
130
|
+
" id INTEGER PRIMARY KEY,",
|
|
131
|
+
" email TEXT NOT NULL UNIQUE",
|
|
132
|
+
")",
|
|
133
|
+
].join("\n"),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("adds REFERENCES for foreign key columns", () => {
|
|
138
|
+
expect(
|
|
139
|
+
postgres(
|
|
140
|
+
annotate.named(
|
|
141
|
+
shapes.mapping({
|
|
142
|
+
id: annotate.primary(shapes.int()),
|
|
143
|
+
user_id: annotate.foreign(shapes.int(), "User", "id"),
|
|
144
|
+
}),
|
|
145
|
+
"Post",
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
).toBe(
|
|
149
|
+
[
|
|
150
|
+
"CREATE TABLE IF NOT EXISTS Post (",
|
|
151
|
+
" id INTEGER PRIMARY KEY,",
|
|
152
|
+
" user_id INTEGER NOT NULL REFERENCES User(id)",
|
|
153
|
+
")",
|
|
154
|
+
].join("\n"),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns column type for unnamed mappings", () => {
|
|
159
|
+
expect(postgres(shapes.mapping({ x: shapes.int() }))).toBe("JSONB");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Shape } from "../shape";
|
|
2
|
+
import { TranslationFunc } from "./translation";
|
|
3
|
+
|
|
4
|
+
const pgType = (shape: Shape): string => {
|
|
5
|
+
switch (shape.type) {
|
|
6
|
+
case "int":
|
|
7
|
+
return "INTEGER";
|
|
8
|
+
case "float":
|
|
9
|
+
return "DOUBLE PRECISION";
|
|
10
|
+
case "number":
|
|
11
|
+
return "NUMERIC";
|
|
12
|
+
case "str":
|
|
13
|
+
return "TEXT";
|
|
14
|
+
case "bool":
|
|
15
|
+
return "BOOLEAN";
|
|
16
|
+
case "date":
|
|
17
|
+
return "TIMESTAMPTZ";
|
|
18
|
+
case "nil":
|
|
19
|
+
return "NULL";
|
|
20
|
+
case "unknown":
|
|
21
|
+
return "JSONB";
|
|
22
|
+
case "literal-int":
|
|
23
|
+
case "literal-float":
|
|
24
|
+
case "literal-number":
|
|
25
|
+
return "NUMERIC";
|
|
26
|
+
case "literal-str":
|
|
27
|
+
return "TEXT";
|
|
28
|
+
case "literal-bool":
|
|
29
|
+
return "BOOLEAN";
|
|
30
|
+
case "ref":
|
|
31
|
+
return shape.name;
|
|
32
|
+
case "array": {
|
|
33
|
+
const inner = pgType(shape.item);
|
|
34
|
+
return inner === "JSONB" ? "JSONB" : `${inner}[]`;
|
|
35
|
+
}
|
|
36
|
+
case "tuple":
|
|
37
|
+
return "JSONB";
|
|
38
|
+
case "union":
|
|
39
|
+
return "JSONB";
|
|
40
|
+
case "vector":
|
|
41
|
+
return `vector(${shape.dims})`;
|
|
42
|
+
case "mapping":
|
|
43
|
+
return "JSONB";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const pgColumnDef = (colName: string, shape: Shape): string => {
|
|
48
|
+
const parts: string[] = [colName, pgType(shape)];
|
|
49
|
+
if (shape.anno.primary) {
|
|
50
|
+
parts.push("PRIMARY KEY");
|
|
51
|
+
} else if (!shape.anno.optional) {
|
|
52
|
+
parts.push("NOT NULL");
|
|
53
|
+
}
|
|
54
|
+
if (shape.anno.unique && !shape.anno.primary) {
|
|
55
|
+
parts.push("UNIQUE");
|
|
56
|
+
}
|
|
57
|
+
if (shape.anno.foreign) {
|
|
58
|
+
parts.push(
|
|
59
|
+
`REFERENCES ${shape.anno.foreign.shapeName}(${shape.anno.foreign.fieldName})`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return parts.join(" ");
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const postgres: TranslationFunc<string> = (shape) => {
|
|
66
|
+
if (shape.type === "mapping" && shape.anno.name) {
|
|
67
|
+
const cols = Object.entries(shape.rec)
|
|
68
|
+
.map(([k, v]) => ` ${pgColumnDef(k, v)}`)
|
|
69
|
+
.join(",\n");
|
|
70
|
+
return `CREATE TABLE IF NOT EXISTS ${shape.anno.name} (\n${cols}\n)`;
|
|
71
|
+
}
|
|
72
|
+
return pgType(shape);
|
|
73
|
+
};
|