effect-qb 0.15.0 → 0.17.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/dist/mysql.js +1957 -595
- package/dist/postgres/metadata.js +2507 -182
- package/dist/postgres.js +9587 -8201
- package/dist/sqlite.js +8360 -0
- package/package.json +7 -2
- package/src/internal/column-state.ts +7 -0
- package/src/internal/column.ts +22 -0
- package/src/internal/derived-table.ts +29 -3
- package/src/internal/dialect.ts +14 -1
- package/src/internal/dsl-mutation-runtime.ts +173 -4
- package/src/internal/dsl-plan-runtime.ts +165 -20
- package/src/internal/dsl-query-runtime.ts +60 -6
- package/src/internal/dsl-transaction-ddl-runtime.ts +72 -2
- package/src/internal/executor.ts +62 -13
- package/src/internal/expression-ast.ts +3 -2
- package/src/internal/grouping-key.ts +141 -1
- package/src/internal/implication-runtime.ts +2 -1
- package/src/internal/json/types.ts +155 -40
- package/src/internal/predicate/analysis.ts +103 -1
- package/src/internal/predicate/atom.ts +7 -0
- package/src/internal/predicate/context.ts +170 -17
- package/src/internal/predicate/key.ts +64 -2
- package/src/internal/predicate/normalize.ts +115 -34
- package/src/internal/predicate/runtime.ts +144 -13
- package/src/internal/query.ts +563 -103
- package/src/internal/renderer.ts +39 -2
- package/src/internal/runtime/driver-value-mapping.ts +244 -0
- package/src/internal/runtime/normalize.ts +62 -38
- package/src/internal/runtime/schema.ts +5 -3
- package/src/internal/runtime/value.ts +153 -30
- package/src/internal/scalar.ts +11 -0
- package/src/internal/table-options.ts +108 -1
- package/src/internal/table.ts +87 -29
- package/src/mysql/column.ts +19 -2
- package/src/mysql/datatypes/index.ts +21 -0
- package/src/mysql/errors/catalog.ts +5 -5
- package/src/mysql/errors/normalize.ts +2 -2
- package/src/mysql/executor.ts +20 -5
- package/src/mysql/internal/dialect.ts +12 -6
- package/src/mysql/internal/dsl.ts +995 -263
- package/src/mysql/internal/renderer.ts +13 -3
- package/src/mysql/internal/sql-expression-renderer.ts +530 -128
- package/src/mysql/query.ts +9 -2
- package/src/mysql/renderer.ts +7 -2
- package/src/mysql/table.ts +38 -12
- package/src/postgres/cast.ts +22 -7
- package/src/postgres/column.ts +5 -2
- package/src/postgres/errors/normalize.ts +2 -2
- package/src/postgres/executor.ts +68 -10
- package/src/postgres/function/core.ts +19 -1
- package/src/postgres/internal/dialect.ts +12 -6
- package/src/postgres/internal/dsl.ts +958 -288
- package/src/postgres/internal/renderer.ts +13 -3
- package/src/postgres/internal/schema-ddl.ts +2 -1
- package/src/postgres/internal/schema-model.ts +6 -3
- package/src/postgres/internal/sql-expression-renderer.ts +477 -96
- package/src/postgres/json.ts +57 -17
- package/src/postgres/query.ts +9 -2
- package/src/postgres/renderer.ts +7 -2
- package/src/postgres/schema-management.ts +91 -4
- package/src/postgres/schema.ts +1 -1
- package/src/postgres/table.ts +189 -53
- package/src/postgres/type.ts +4 -0
- package/src/sqlite/column.ts +128 -0
- package/src/sqlite/datatypes/index.ts +79 -0
- package/src/sqlite/datatypes/spec.ts +98 -0
- package/src/sqlite/errors/catalog.ts +103 -0
- package/src/sqlite/errors/fields.ts +19 -0
- package/src/sqlite/errors/index.ts +19 -0
- package/src/sqlite/errors/normalize.ts +229 -0
- package/src/sqlite/errors/requirements.ts +71 -0
- package/src/sqlite/errors/types.ts +29 -0
- package/src/sqlite/executor.ts +227 -0
- package/src/sqlite/function/aggregate.ts +2 -0
- package/src/sqlite/function/core.ts +2 -0
- package/src/sqlite/function/index.ts +19 -0
- package/src/sqlite/function/string.ts +2 -0
- package/src/sqlite/function/temporal.ts +100 -0
- package/src/sqlite/function/window.ts +2 -0
- package/src/sqlite/internal/dialect.ts +37 -0
- package/src/sqlite/internal/dsl.ts +6926 -0
- package/src/sqlite/internal/renderer.ts +47 -0
- package/src/sqlite/internal/sql-expression-renderer.ts +1821 -0
- package/src/sqlite/json.ts +2 -0
- package/src/sqlite/query.ts +196 -0
- package/src/sqlite/renderer.ts +24 -0
- package/src/sqlite/table.ts +183 -0
- package/src/sqlite.ts +22 -0
package/src/internal/renderer.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as Query from "./query.js"
|
|
2
|
-
import
|
|
2
|
+
import type * as Expression from "./scalar.js"
|
|
3
|
+
import { flattenSelection, type Projection, validateProjections } from "./projections.js"
|
|
4
|
+
import * as Plan from "./row-set.js"
|
|
3
5
|
|
|
4
6
|
/** Symbol used to attach rendered-query phantom row metadata. */
|
|
5
7
|
export const TypeId: unique symbol = Symbol.for("effect-qb/Renderer")
|
|
@@ -21,6 +23,7 @@ export interface RenderedQuery<Row, Dialect extends string = string> {
|
|
|
21
23
|
readonly params: readonly unknown[]
|
|
22
24
|
readonly dialect: Dialect
|
|
23
25
|
readonly projections: readonly Projection[]
|
|
26
|
+
readonly valueMappings?: Expression.DriverValueMappings
|
|
24
27
|
readonly [TypeId]: {
|
|
25
28
|
readonly row: Row
|
|
26
29
|
readonly dialect: Dialect
|
|
@@ -42,7 +45,7 @@ export interface Renderer<Dialect extends string = string> {
|
|
|
42
45
|
readonly dialect: Dialect
|
|
43
46
|
render<PlanValue extends Query.Plan.Any>(
|
|
44
47
|
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
45
|
-
): RenderedQuery<
|
|
48
|
+
): RenderedQuery<Query.ResultRow<PlanValue>, Dialect>
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
type CustomRender<Dialect extends string> = <PlanValue extends Query.Plan.Any>(
|
|
@@ -51,6 +54,30 @@ type CustomRender<Dialect extends string> = <PlanValue extends Query.Plan.Any>(
|
|
|
51
54
|
readonly sql: string
|
|
52
55
|
readonly params?: readonly unknown[]
|
|
53
56
|
readonly projections?: readonly Projection[]
|
|
57
|
+
readonly valueMappings?: Expression.DriverValueMappings
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const projectionPathKey = (path: readonly string[]): string => JSON.stringify(path)
|
|
61
|
+
|
|
62
|
+
const formatProjectionPath = (path: readonly string[]): string => path.join(".")
|
|
63
|
+
|
|
64
|
+
const validateProjectionPathsMatchSelection = (
|
|
65
|
+
plan: Query.Plan.Any,
|
|
66
|
+
projections: readonly Projection[]
|
|
67
|
+
): void => {
|
|
68
|
+
const expected = flattenSelection(Query.getAst(plan).select as Record<string, unknown>)
|
|
69
|
+
const expectedPaths = new Set(expected.map((projection) => projectionPathKey(projection.path)))
|
|
70
|
+
const actualPaths = new Set(projections.map((projection) => projectionPathKey(projection.path)))
|
|
71
|
+
for (const projection of projections) {
|
|
72
|
+
if (!expectedPaths.has(projectionPathKey(projection.path))) {
|
|
73
|
+
throw new Error(`Projection path ${formatProjectionPath(projection.path)} does not exist in the query selection`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const projection of expected) {
|
|
77
|
+
if (!actualPaths.has(projectionPathKey(projection.path))) {
|
|
78
|
+
throw new Error(`Projection path ${formatProjectionPath(projection.path)} is missing from rendered projections`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
/**
|
|
@@ -70,13 +97,23 @@ export function make<Dialect extends string>(
|
|
|
70
97
|
return {
|
|
71
98
|
dialect,
|
|
72
99
|
render(plan) {
|
|
100
|
+
const required = Query.currentRequiredList(plan[Plan.TypeId].required)
|
|
101
|
+
if (required.length > 0) {
|
|
102
|
+
throw new Error(`query references sources that are not yet in scope: ${required.join(", ")}`)
|
|
103
|
+
}
|
|
104
|
+
const planDialect = plan[Plan.TypeId].dialect
|
|
105
|
+
if (planDialect !== dialect) {
|
|
106
|
+
throw new Error("effect-qb: plan dialect is not compatible with the target renderer or executor")
|
|
107
|
+
}
|
|
73
108
|
const rendered = render(plan)
|
|
74
109
|
const projections = rendered.projections ?? []
|
|
75
110
|
validateProjections(projections)
|
|
111
|
+
validateProjectionPathsMatchSelection(plan as Query.Plan.Any, projections)
|
|
76
112
|
return {
|
|
77
113
|
sql: rendered.sql,
|
|
78
114
|
params: rendered.params ?? [],
|
|
79
115
|
projections,
|
|
116
|
+
valueMappings: rendered.valueMappings,
|
|
80
117
|
dialect,
|
|
81
118
|
[TypeId]: {
|
|
82
119
|
row: undefined as any,
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema"
|
|
2
|
+
|
|
3
|
+
import type * as Expression from "../scalar.js"
|
|
4
|
+
import { normalizeDbValue } from "./normalize.js"
|
|
5
|
+
|
|
6
|
+
export type DriverValueMapping = Expression.DriverValueMapping
|
|
7
|
+
export type DriverValueMappings = Expression.DriverValueMappings
|
|
8
|
+
|
|
9
|
+
export interface DriverValueContext {
|
|
10
|
+
readonly dialect?: string
|
|
11
|
+
readonly dbType?: Expression.DbType.Any
|
|
12
|
+
readonly runtimeSchema?: Schema.Schema.Any
|
|
13
|
+
readonly driverValueMapping?: DriverValueMapping
|
|
14
|
+
readonly valueMappings?: DriverValueMappings
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type MappingKey =
|
|
18
|
+
| "fromDriver"
|
|
19
|
+
| "toDriver"
|
|
20
|
+
| "selectSql"
|
|
21
|
+
| "jsonSelectSql"
|
|
22
|
+
|
|
23
|
+
const runtimeTagOfDbType = (
|
|
24
|
+
dbType: Expression.DbType.Any | undefined
|
|
25
|
+
): string | undefined => {
|
|
26
|
+
if (dbType === undefined) {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
if ("base" in dbType) {
|
|
30
|
+
return runtimeTagOfDbType(dbType.base)
|
|
31
|
+
}
|
|
32
|
+
if ("element" in dbType) {
|
|
33
|
+
return "array"
|
|
34
|
+
}
|
|
35
|
+
if ("fields" in dbType) {
|
|
36
|
+
return "record"
|
|
37
|
+
}
|
|
38
|
+
if ("variant" in dbType && dbType.variant === "json") {
|
|
39
|
+
return "json"
|
|
40
|
+
}
|
|
41
|
+
if ("variant" in dbType && (dbType.variant === "enum" || dbType.variant === "set")) {
|
|
42
|
+
return "string"
|
|
43
|
+
}
|
|
44
|
+
return dbType.runtime
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const familyOfDbType = (
|
|
48
|
+
dbType: Expression.DbType.Any | undefined
|
|
49
|
+
): string | undefined => {
|
|
50
|
+
if (dbType === undefined) {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
if ("base" in dbType) {
|
|
54
|
+
return familyOfDbType(dbType.base)
|
|
55
|
+
}
|
|
56
|
+
return dbType.family
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const mappingCandidates = (
|
|
60
|
+
context: DriverValueContext
|
|
61
|
+
): readonly (DriverValueMapping | undefined)[] => {
|
|
62
|
+
const dbType = context.dbType
|
|
63
|
+
const runtimeTag = runtimeTagOfDbType(dbType)
|
|
64
|
+
const family = familyOfDbType(dbType)
|
|
65
|
+
return [
|
|
66
|
+
context.driverValueMapping,
|
|
67
|
+
dbType?.driverValueMapping,
|
|
68
|
+
dbType === undefined ? undefined : context.valueMappings?.[dbType.kind],
|
|
69
|
+
family === undefined ? undefined : context.valueMappings?.[family],
|
|
70
|
+
runtimeTag === undefined ? undefined : context.valueMappings?.[runtimeTag]
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const findMapping = <Key extends MappingKey>(
|
|
75
|
+
context: DriverValueContext,
|
|
76
|
+
key: Key
|
|
77
|
+
): NonNullable<DriverValueMapping[Key]> | undefined => {
|
|
78
|
+
for (const candidate of mappingCandidates(context)) {
|
|
79
|
+
const value = candidate?.[key]
|
|
80
|
+
if (value !== undefined) {
|
|
81
|
+
return value as NonNullable<DriverValueMapping[Key]>
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isJsonDbType = (dbType: Expression.DbType.Any | undefined): boolean => {
|
|
88
|
+
if (dbType === undefined) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
if ("base" in dbType) {
|
|
92
|
+
return isJsonDbType(dbType.base)
|
|
93
|
+
}
|
|
94
|
+
if (!("variant" in dbType)) {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
const variant = dbType.variant as string
|
|
98
|
+
return variant === "json" || variant === "jsonb"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const schemaAccepts = (
|
|
102
|
+
schema: Schema.Schema.Any | undefined,
|
|
103
|
+
value: unknown
|
|
104
|
+
): boolean =>
|
|
105
|
+
schema !== undefined && (Schema.is(schema) as (candidate: unknown) => boolean)(value)
|
|
106
|
+
|
|
107
|
+
const encodeWithSchema = (
|
|
108
|
+
schema: Schema.Schema.Any | undefined,
|
|
109
|
+
value: unknown
|
|
110
|
+
): { readonly value: unknown; readonly encoded: boolean } => {
|
|
111
|
+
if (schema === undefined) {
|
|
112
|
+
return { value, encoded: false }
|
|
113
|
+
}
|
|
114
|
+
if (!(Schema.is(schema) as (value: unknown) => boolean)(value)) {
|
|
115
|
+
return { value, encoded: false }
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
value: (Schema.encodeUnknownSync as any)(schema)(value),
|
|
119
|
+
encoded: true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const normalizeJsonDriverString = (
|
|
124
|
+
value: string,
|
|
125
|
+
context: DriverValueContext
|
|
126
|
+
): unknown | undefined => {
|
|
127
|
+
if (!isJsonDbType(context.dbType) || context.runtimeSchema === undefined) {
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(value)
|
|
132
|
+
if (value.trimStart().startsWith("\"") && schemaAccepts(context.runtimeSchema, parsed)) {
|
|
133
|
+
return parsed
|
|
134
|
+
}
|
|
135
|
+
if (schemaAccepts(context.runtimeSchema, value) && !schemaAccepts(context.runtimeSchema, parsed)) {
|
|
136
|
+
return value
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error instanceof SyntaxError && schemaAccepts(context.runtimeSchema, value)) {
|
|
140
|
+
return value
|
|
141
|
+
}
|
|
142
|
+
if (!(error instanceof SyntaxError)) {
|
|
143
|
+
throw error
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const toDriverValue = (
|
|
150
|
+
value: unknown,
|
|
151
|
+
context: DriverValueContext
|
|
152
|
+
): unknown => {
|
|
153
|
+
if (value === null) {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
if (value instanceof Date && Number.isNaN(value.getTime())) {
|
|
157
|
+
throw new Error("Expected a valid Date value")
|
|
158
|
+
}
|
|
159
|
+
const dbType = context.dbType
|
|
160
|
+
const encoded = encodeWithSchema(context.runtimeSchema, value)
|
|
161
|
+
let current = encoded.value
|
|
162
|
+
const custom = findMapping(context, "toDriver")
|
|
163
|
+
if (custom !== undefined && dbType !== undefined) {
|
|
164
|
+
return custom(current, dbType)
|
|
165
|
+
}
|
|
166
|
+
if (encoded.encoded && typeof current === "string" && isJsonDbType(dbType)) {
|
|
167
|
+
return current
|
|
168
|
+
}
|
|
169
|
+
return dbType === undefined || !encoded.encoded
|
|
170
|
+
? current
|
|
171
|
+
: normalizeDbValue(dbType, current)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const fromDriverValue = (
|
|
175
|
+
value: unknown,
|
|
176
|
+
context: DriverValueContext
|
|
177
|
+
): unknown => {
|
|
178
|
+
if (value === null) {
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
const dbType = context.dbType
|
|
182
|
+
const custom = findMapping(context, "fromDriver")
|
|
183
|
+
if (custom !== undefined && dbType !== undefined) {
|
|
184
|
+
return custom(value, dbType)
|
|
185
|
+
}
|
|
186
|
+
if (typeof value === "string") {
|
|
187
|
+
const normalizedJsonString = normalizeJsonDriverString(value, context)
|
|
188
|
+
if (normalizedJsonString !== undefined) {
|
|
189
|
+
return normalizedJsonString
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return dbType === undefined
|
|
193
|
+
? value
|
|
194
|
+
: normalizeDbValue(dbType, value)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const textCast = (sql: string): string => `(${sql})::text`
|
|
198
|
+
|
|
199
|
+
const postgresJsonSql = (
|
|
200
|
+
sql: string,
|
|
201
|
+
dbType: Expression.DbType.Any
|
|
202
|
+
): string => {
|
|
203
|
+
const runtimeTag = runtimeTagOfDbType(dbType)
|
|
204
|
+
switch (runtimeTag) {
|
|
205
|
+
case "bigintString":
|
|
206
|
+
case "decimalString":
|
|
207
|
+
case "localDate":
|
|
208
|
+
case "localTime":
|
|
209
|
+
case "offsetTime":
|
|
210
|
+
case "localDateTime":
|
|
211
|
+
case "instant":
|
|
212
|
+
case "year":
|
|
213
|
+
return textCast(sql)
|
|
214
|
+
case "bytes":
|
|
215
|
+
return `encode(${sql}, 'base64')`
|
|
216
|
+
default:
|
|
217
|
+
return sql
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const renderSelectSql = (
|
|
222
|
+
sql: string,
|
|
223
|
+
context: DriverValueContext
|
|
224
|
+
): string => {
|
|
225
|
+
const dbType = context.dbType
|
|
226
|
+
const custom = findMapping(context, "selectSql")
|
|
227
|
+
return custom !== undefined && dbType !== undefined
|
|
228
|
+
? custom(sql, dbType)
|
|
229
|
+
: sql
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const renderJsonSelectSql = (
|
|
233
|
+
sql: string,
|
|
234
|
+
context: DriverValueContext
|
|
235
|
+
): string => {
|
|
236
|
+
const dbType = context.dbType
|
|
237
|
+
const custom = findMapping(context, "jsonSelectSql")
|
|
238
|
+
if (custom !== undefined && dbType !== undefined) {
|
|
239
|
+
return custom(sql, dbType)
|
|
240
|
+
}
|
|
241
|
+
return context.dialect === "postgres" && dbType !== undefined
|
|
242
|
+
? postgresJsonSql(sql, dbType)
|
|
243
|
+
: sql
|
|
244
|
+
}
|
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import type * as Expression from "../scalar.js"
|
|
2
2
|
import type { RuntimeTag } from "../datatypes/shape.js"
|
|
3
|
+
import {
|
|
4
|
+
canonicalizeBigIntString,
|
|
5
|
+
canonicalizeDecimalString,
|
|
6
|
+
isValidInstantString,
|
|
7
|
+
isValidLocalDateString,
|
|
8
|
+
isValidLocalDateTimeString,
|
|
9
|
+
isValidLocalTimeString,
|
|
10
|
+
isValidOffsetTimeString
|
|
11
|
+
} from "./value.js"
|
|
3
12
|
|
|
4
13
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
5
14
|
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
6
15
|
|
|
16
|
+
const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
|
|
17
|
+
if (!isRecord(value)) {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
const prototype = Object.getPrototypeOf(value)
|
|
21
|
+
return prototype === Object.prototype || prototype === null
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
const pad = (value: number, width = 2): string => value.toString().padStart(width, "0")
|
|
8
25
|
|
|
9
26
|
const formatLocalDate = (value: Date): string =>
|
|
10
|
-
`${value.getUTCFullYear()}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`
|
|
27
|
+
`${pad(value.getUTCFullYear(), 4)}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`
|
|
11
28
|
|
|
12
29
|
const formatLocalTime = (value: Date): string => {
|
|
13
30
|
const milliseconds = value.getUTCMilliseconds()
|
|
@@ -34,12 +51,17 @@ const expectString = (value: unknown, label: string): string => {
|
|
|
34
51
|
throw new Error(`Expected ${label} as string`)
|
|
35
52
|
}
|
|
36
53
|
|
|
54
|
+
const finiteNumberStringPattern = /^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:[eE][+-]?\d+)?$/
|
|
55
|
+
|
|
37
56
|
const normalizeNumber = (value: unknown): number => {
|
|
38
57
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
39
58
|
return value
|
|
40
59
|
}
|
|
41
|
-
if (typeof value === "string"
|
|
42
|
-
const
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
const trimmed = value.trim()
|
|
62
|
+
const parsed = finiteNumberStringPattern.test(trimmed)
|
|
63
|
+
? Number(trimmed)
|
|
64
|
+
: Number.NaN
|
|
43
65
|
if (Number.isFinite(parsed)) {
|
|
44
66
|
return parsed
|
|
45
67
|
}
|
|
@@ -81,27 +103,12 @@ const normalizeBigIntString = (value: unknown): string => {
|
|
|
81
103
|
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
82
104
|
return BigInt(value).toString()
|
|
83
105
|
}
|
|
84
|
-
if (typeof value === "string"
|
|
85
|
-
return
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
return canonicalizeBigIntString(value)
|
|
86
108
|
}
|
|
87
109
|
throw new Error("Expected an integer-like bigint value")
|
|
88
110
|
}
|
|
89
111
|
|
|
90
|
-
const canonicalizeDecimalString = (input: string): string => {
|
|
91
|
-
const trimmed = input.trim()
|
|
92
|
-
const match = /^([+-]?)(\d+)(?:\.(\d+))?$/.exec(trimmed)
|
|
93
|
-
if (match === null) {
|
|
94
|
-
throw new Error("Expected a decimal string")
|
|
95
|
-
}
|
|
96
|
-
const sign = match[1] === "-" ? "-" : ""
|
|
97
|
-
const integer = match[2]!.replace(/^0+(?=\d)/, "") || "0"
|
|
98
|
-
const fraction = (match[3] ?? "").replace(/0+$/, "")
|
|
99
|
-
if (fraction.length === 0) {
|
|
100
|
-
return `${sign}${integer}`
|
|
101
|
-
}
|
|
102
|
-
return `${sign}${integer}.${fraction}`
|
|
103
|
-
}
|
|
104
|
-
|
|
105
112
|
const normalizeDecimalString = (value: unknown): string => {
|
|
106
113
|
if (typeof value === "string") {
|
|
107
114
|
return canonicalizeDecimalString(value)
|
|
@@ -121,12 +128,15 @@ const normalizeLocalDate = (value: unknown): string => {
|
|
|
121
128
|
return formatLocalDate(value)
|
|
122
129
|
}
|
|
123
130
|
const raw = expectString(value, "local date").trim()
|
|
124
|
-
if (
|
|
131
|
+
if (isValidLocalDateString(raw)) {
|
|
125
132
|
return raw
|
|
126
133
|
}
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
|
|
134
|
+
const canonicalInstant = raw.replace(" ", "T").replace(/z$/, "Z")
|
|
135
|
+
if (isValidInstantString(canonicalInstant)) {
|
|
136
|
+
const parsed = new Date(canonicalInstant)
|
|
137
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
138
|
+
return formatLocalDate(parsed)
|
|
139
|
+
}
|
|
130
140
|
}
|
|
131
141
|
throw new Error("Expected a local-date value")
|
|
132
142
|
}
|
|
@@ -136,7 +146,7 @@ const normalizeLocalTime = (value: unknown): string => {
|
|
|
136
146
|
return formatLocalTime(value)
|
|
137
147
|
}
|
|
138
148
|
const raw = expectString(value, "local time").trim()
|
|
139
|
-
if (
|
|
149
|
+
if (isValidLocalTimeString(raw)) {
|
|
140
150
|
return raw
|
|
141
151
|
}
|
|
142
152
|
throw new Error("Expected a local-time value")
|
|
@@ -147,7 +157,7 @@ const normalizeOffsetTime = (value: unknown): string => {
|
|
|
147
157
|
return `${formatLocalTime(value)}Z`
|
|
148
158
|
}
|
|
149
159
|
const raw = expectString(value, "offset time").trim()
|
|
150
|
-
if (
|
|
160
|
+
if (isValidOffsetTimeString(raw)) {
|
|
151
161
|
return raw
|
|
152
162
|
}
|
|
153
163
|
throw new Error("Expected an offset-time value")
|
|
@@ -158,11 +168,13 @@ const normalizeLocalDateTime = (value: unknown): string => {
|
|
|
158
168
|
return formatLocalDateTime(value)
|
|
159
169
|
}
|
|
160
170
|
const raw = expectString(value, "local datetime").trim()
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
const canonicalLocalDateTime = raw.replace(" ", "T")
|
|
172
|
+
if (isValidLocalDateTimeString(canonicalLocalDateTime)) {
|
|
173
|
+
return canonicalLocalDateTime
|
|
163
174
|
}
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
const canonicalInstant = raw.replace(" ", "T").replace(/z$/, "Z")
|
|
176
|
+
if (isValidInstantString(canonicalInstant)) {
|
|
177
|
+
const parsed = new Date(canonicalInstant)
|
|
166
178
|
if (!Number.isNaN(parsed.getTime())) {
|
|
167
179
|
return formatLocalDateTime(parsed)
|
|
168
180
|
}
|
|
@@ -178,7 +190,11 @@ const normalizeInstant = (value: unknown): string => {
|
|
|
178
190
|
if (!/[zZ]|[+-]\d{2}:\d{2}$/.test(raw)) {
|
|
179
191
|
throw new Error("Instant values require a timezone offset")
|
|
180
192
|
}
|
|
181
|
-
const
|
|
193
|
+
const canonicalInstant = raw.replace(" ", "T").replace(/z$/, "Z")
|
|
194
|
+
if (!isValidInstantString(canonicalInstant)) {
|
|
195
|
+
throw new Error("Expected an ISO instant value")
|
|
196
|
+
}
|
|
197
|
+
const parsed = new Date(canonicalInstant)
|
|
182
198
|
if (Number.isNaN(parsed.getTime())) {
|
|
183
199
|
throw new Error("Expected an ISO instant value")
|
|
184
200
|
}
|
|
@@ -209,20 +225,21 @@ const normalizeBytes = (value: unknown): Uint8Array => {
|
|
|
209
225
|
throw new Error("Expected a byte array value")
|
|
210
226
|
}
|
|
211
227
|
|
|
212
|
-
const isJsonValue = (value: unknown): boolean => {
|
|
228
|
+
export const isJsonValue = (value: unknown): boolean => {
|
|
213
229
|
if (value === null) {
|
|
214
230
|
return true
|
|
215
231
|
}
|
|
216
232
|
switch (typeof value) {
|
|
217
233
|
case "string":
|
|
218
|
-
case "number":
|
|
219
234
|
case "boolean":
|
|
220
235
|
return true
|
|
236
|
+
case "number":
|
|
237
|
+
return Number.isFinite(value)
|
|
221
238
|
case "object":
|
|
222
239
|
if (Array.isArray(value)) {
|
|
223
240
|
return value.every(isJsonValue)
|
|
224
241
|
}
|
|
225
|
-
return
|
|
242
|
+
return isPlainRecord(value) && Object.values(value).every(isJsonValue)
|
|
226
243
|
default:
|
|
227
244
|
return false
|
|
228
245
|
}
|
|
@@ -230,11 +247,18 @@ const isJsonValue = (value: unknown): boolean => {
|
|
|
230
247
|
|
|
231
248
|
const normalizeJson = (value: unknown): unknown => {
|
|
232
249
|
if (typeof value === "string") {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
try {
|
|
251
|
+
const parsed = JSON.parse(value)
|
|
252
|
+
if (isJsonValue(parsed)) {
|
|
253
|
+
return parsed
|
|
254
|
+
}
|
|
255
|
+
throw new Error("Parsed JSON value is not a valid JSON runtime")
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof SyntaxError) {
|
|
258
|
+
return value
|
|
259
|
+
}
|
|
260
|
+
throw error
|
|
236
261
|
}
|
|
237
|
-
throw new Error("Parsed JSON value is not a valid JSON runtime")
|
|
238
262
|
}
|
|
239
263
|
if (isJsonValue(value)) {
|
|
240
264
|
return value
|
|
@@ -38,12 +38,14 @@ const schemaCache = new WeakMap<Expression.Any, RuntimeSchema | undefined>()
|
|
|
38
38
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
39
39
|
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
40
40
|
|
|
41
|
+
const FiniteNumberSchema = Schema.Number.pipe(Schema.finite())
|
|
42
|
+
|
|
41
43
|
const runtimeSchemaForTag = (tag: RuntimeTag): RuntimeSchema | undefined => {
|
|
42
44
|
switch (tag) {
|
|
43
45
|
case "string":
|
|
44
46
|
return Schema.String
|
|
45
47
|
case "number":
|
|
46
|
-
return
|
|
48
|
+
return FiniteNumberSchema
|
|
47
49
|
case "bigintString":
|
|
48
50
|
return BigIntStringSchema
|
|
49
51
|
case "boolean":
|
|
@@ -388,7 +390,7 @@ const deriveRuntimeSchema = (
|
|
|
388
390
|
return Schema.String
|
|
389
391
|
case "count":
|
|
390
392
|
case "jsonLength":
|
|
391
|
-
return
|
|
393
|
+
return FiniteNumberSchema
|
|
392
394
|
case "max":
|
|
393
395
|
case "min":
|
|
394
396
|
return expressionRuntimeSchema(ast.value, context)
|
|
@@ -404,7 +406,7 @@ const deriveRuntimeSchema = (
|
|
|
404
406
|
case "window":
|
|
405
407
|
return ast.function === "over" && ast.value !== undefined
|
|
406
408
|
? expressionRuntimeSchema(ast.value, context)
|
|
407
|
-
:
|
|
409
|
+
: FiniteNumberSchema
|
|
408
410
|
case "jsonGet":
|
|
409
411
|
case "jsonPath":
|
|
410
412
|
case "jsonAccess":
|