@nmtjs/contract 0.0.1
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.md +7 -0
- package/README.md +9 -0
- package/dist/compiler.js +50 -0
- package/dist/compiler.js.map +1 -0
- package/dist/contract.js +24 -0
- package/dist/contract.js.map +1 -0
- package/dist/formats.js +127 -0
- package/dist/formats.js.map +1 -0
- package/dist/guards/blob.js +3 -0
- package/dist/guards/blob.js.map +1 -0
- package/dist/guards/event.js +3 -0
- package/dist/guards/event.js.map +1 -0
- package/dist/guards/native-enum.js +3 -0
- package/dist/guards/native-enum.js.map +1 -0
- package/dist/guards/nullable.js +2 -0
- package/dist/guards/nullable.js.map +1 -0
- package/dist/guards/procedure.js +3 -0
- package/dist/guards/procedure.js.map +1 -0
- package/dist/guards/service.js +3 -0
- package/dist/guards/service.js.map +1 -0
- package/dist/guards/subscription.js +3 -0
- package/dist/guards/subscription.js.map +1 -0
- package/dist/guards/union-enum.js +3 -0
- package/dist/guards/union-enum.js.map +1 -0
- package/dist/guards.js +20 -0
- package/dist/guards.js.map +1 -0
- package/dist/schemas/blob.js +26 -0
- package/dist/schemas/blob.js.map +1 -0
- package/dist/schemas/event.js +12 -0
- package/dist/schemas/event.js.map +1 -0
- package/dist/schemas/native-enum.js +14 -0
- package/dist/schemas/native-enum.js.map +1 -0
- package/dist/schemas/nullable.js +5 -0
- package/dist/schemas/nullable.js.map +1 -0
- package/dist/schemas/procedure.js +14 -0
- package/dist/schemas/procedure.js.map +1 -0
- package/dist/schemas/service.js +35 -0
- package/dist/schemas/service.js.map +1 -0
- package/dist/schemas/subscription.js +16 -0
- package/dist/schemas/subscription.js.map +1 -0
- package/dist/schemas/union-enum.js +13 -0
- package/dist/schemas/union-enum.js.map +1 -0
- package/dist/utils.js +11 -0
- package/dist/utils.js.map +1 -0
- package/package.json +41 -0
- package/src/compiler.ts +63 -0
- package/src/contract.ts +62 -0
- package/src/formats.ts +181 -0
- package/src/guards/blob.ts +5 -0
- package/src/guards/event.ts +6 -0
- package/src/guards/native-enum.ts +7 -0
- package/src/guards/nullable.ts +14 -0
- package/src/guards/procedure.ts +6 -0
- package/src/guards/service.ts +6 -0
- package/src/guards/subscription.ts +10 -0
- package/src/guards/union-enum.ts +7 -0
- package/src/guards.ts +21 -0
- package/src/schemas/blob.ts +58 -0
- package/src/schemas/event.ts +35 -0
- package/src/schemas/native-enum.ts +37 -0
- package/src/schemas/nullable.ts +4 -0
- package/src/schemas/procedure.ts +61 -0
- package/src/schemas/service.ts +126 -0
- package/src/schemas/subscription.ts +82 -0
- package/src/schemas/union-enum.ts +43 -0
- package/src/utils.ts +16 -0
- package/tsconfig.json +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils.ts"],"sourcesContent":["import type { SchemaOptions, TSchema } from '@sinclair/typebox'\n\nexport type ContractSchemaOptions = Pick<SchemaOptions, 'title' | 'description'>\n\nexport const applyNames = <T extends Record<string, { serviceName?: string }>>(\n params: T,\n opts: { serviceName?: string; subscriptionName?: string },\n) => {\n return Object.fromEntries(\n Object.entries(params).map(([k, v]) => [k, { ...v, name: k, ...opts }]),\n )\n}\n\nexport const createSchema = <T extends TSchema>(\n schema: Omit<T, 'static' | 'params'>,\n) => schema as T\n"],"names":["applyNames","params","opts","Object","fromEntries","entries","map","k","v","name","createSchema","schema"],"mappings":"AAIA,OAAO,MAAMA,aAAa,CACxBC,QACAC;IAEA,OAAOC,OAAOC,WAAW,CACvBD,OAAOE,OAAO,CAACJ,QAAQK,GAAG,CAAC,CAAC,CAACC,GAAGC,EAAE,GAAK;YAACD;YAAG;gBAAE,GAAGC,CAAC;gBAAEC,MAAMF;gBAAG,GAAGL,IAAI;YAAC;SAAE;AAE1E,EAAC;AAED,OAAO,MAAMQ,eAAe,CAC1BC,SACGA,OAAW"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nmtjs/contract",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"bun": "./src/contract.ts",
|
|
7
|
+
"default": "./dist/contract.js",
|
|
8
|
+
"types": "./src/contract.ts"
|
|
9
|
+
},
|
|
10
|
+
"./compiler": {
|
|
11
|
+
"bun": "./src/compiler.ts",
|
|
12
|
+
"default": "./dist/compiler.js",
|
|
13
|
+
"types": "./src/compiler.ts"
|
|
14
|
+
},
|
|
15
|
+
"./guards": {
|
|
16
|
+
"bun": "./src/guards.ts",
|
|
17
|
+
"default": "./dist/guards.js",
|
|
18
|
+
"types": "./src/guards.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@sinclair/typebox": "^0.32.34",
|
|
23
|
+
"@nmtjs/common": "0.0.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@sinclair/typebox": "^0.32.34",
|
|
27
|
+
"@nmtjs/common": "0.0.1"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"dist",
|
|
32
|
+
"tsconfig.json",
|
|
33
|
+
"LICENSE.md",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"version": "0.0.1",
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "neemata-build -p neutral --root=./src './**/*.ts'",
|
|
39
|
+
"type-check": "tsc --noEmit"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type TypeCheck,
|
|
3
|
+
TypeCompiler,
|
|
4
|
+
type ValueErrorIterator,
|
|
5
|
+
} from '@sinclair/typebox/compiler'
|
|
6
|
+
import type { TSchema } from '@sinclair/typebox/type'
|
|
7
|
+
import { Value } from '@sinclair/typebox/value'
|
|
8
|
+
|
|
9
|
+
export type Compiled = {
|
|
10
|
+
check: (val: unknown) => boolean
|
|
11
|
+
errors: (val: unknown) => ValueErrorIterator
|
|
12
|
+
decode: (
|
|
13
|
+
val: unknown,
|
|
14
|
+
) => { success: true; value: unknown } | { success: false; error: any }
|
|
15
|
+
encode: (
|
|
16
|
+
val: unknown,
|
|
17
|
+
) => { success: true; value: unknown } | { success: false; error: any }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const compileSchema = (
|
|
21
|
+
schema: TSchema,
|
|
22
|
+
): TypeCheck<TSchema> & {
|
|
23
|
+
Prepare: (value: any) => unknown
|
|
24
|
+
} => {
|
|
25
|
+
const compiled = TypeCompiler.Compile(schema)
|
|
26
|
+
const Prepare = (value: any) => {
|
|
27
|
+
for (const fn of [Value.Clean, Value.Default]) {
|
|
28
|
+
value = fn(schema, value)
|
|
29
|
+
}
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
return Object.assign(compiled, { Prepare })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const compile = (schema: TSchema): Compiled => {
|
|
36
|
+
const compiled = compileSchema(schema)
|
|
37
|
+
|
|
38
|
+
// TODO: custom error handling/shaping
|
|
39
|
+
return {
|
|
40
|
+
check: compiled.Check.bind(compiled),
|
|
41
|
+
errors: compiled.Errors.bind(compiled),
|
|
42
|
+
decode: (val) => {
|
|
43
|
+
try {
|
|
44
|
+
return {
|
|
45
|
+
success: true as const,
|
|
46
|
+
value: compiled.Decode(compiled.Prepare(val)),
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return { success: false as const, error }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
encode: (val) => {
|
|
53
|
+
try {
|
|
54
|
+
return {
|
|
55
|
+
success: true as const,
|
|
56
|
+
value: compiled.Encode(compiled.Prepare(val)),
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return { success: false as const, error }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/contract.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JsonTypeBuilder,
|
|
3
|
+
Kind,
|
|
4
|
+
type StaticDecode,
|
|
5
|
+
type StaticEncode,
|
|
6
|
+
type TSchema,
|
|
7
|
+
} from '@sinclair/typebox/type'
|
|
8
|
+
import { register } from './formats.ts' // register ajv formats
|
|
9
|
+
|
|
10
|
+
import { BlobType, type TBlob } from './schemas/blob.ts'
|
|
11
|
+
import { EventContract, type TEventContract } from './schemas/event.ts'
|
|
12
|
+
import { NativeEnum, type TNativeEnum } from './schemas/native-enum.ts'
|
|
13
|
+
import { Nullable } from './schemas/nullable.ts'
|
|
14
|
+
import {
|
|
15
|
+
ProcedureContract,
|
|
16
|
+
type TBaseProcedureContract,
|
|
17
|
+
type TProcedureContract,
|
|
18
|
+
} from './schemas/procedure.ts'
|
|
19
|
+
import { ServiceContract, type TServiceContract } from './schemas/service.ts'
|
|
20
|
+
import {
|
|
21
|
+
SubscriptionContract,
|
|
22
|
+
type TSubscriptionContract,
|
|
23
|
+
} from './schemas/subscription.ts'
|
|
24
|
+
import { type TUnionEnum, UnionEnum } from './schemas/union-enum.ts'
|
|
25
|
+
|
|
26
|
+
register()
|
|
27
|
+
|
|
28
|
+
const Contract = Object.freeze({
|
|
29
|
+
Procedure: ProcedureContract,
|
|
30
|
+
Event: EventContract,
|
|
31
|
+
Subscription: SubscriptionContract,
|
|
32
|
+
Service: ServiceContract,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const Type = Object.freeze(
|
|
36
|
+
Object.assign(new JsonTypeBuilder(), {
|
|
37
|
+
UnionEnum,
|
|
38
|
+
NativeEnum,
|
|
39
|
+
Nullable,
|
|
40
|
+
Blob: BlobType,
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
type Encoded<T extends TSchema> = StaticEncode<T>
|
|
45
|
+
type Decoded<T extends TSchema> = StaticDecode<T>
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
Contract,
|
|
49
|
+
Kind,
|
|
50
|
+
Type,
|
|
51
|
+
type Decoded,
|
|
52
|
+
type Encoded,
|
|
53
|
+
type TBlob,
|
|
54
|
+
type TEventContract,
|
|
55
|
+
type TProcedureContract,
|
|
56
|
+
type TBaseProcedureContract,
|
|
57
|
+
type TSchema,
|
|
58
|
+
type TServiceContract,
|
|
59
|
+
type TSubscriptionContract,
|
|
60
|
+
type TUnionEnum,
|
|
61
|
+
type TNativeEnum,
|
|
62
|
+
}
|
package/src/formats.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { FormatRegistry } from '@sinclair/typebox/type'
|
|
2
|
+
|
|
3
|
+
// TODO: review all of this
|
|
4
|
+
|
|
5
|
+
export const register = () => {
|
|
6
|
+
for (const [name, format] of Object.entries(fullFormats)) {
|
|
7
|
+
if (format === true) {
|
|
8
|
+
FormatRegistry.Set(name, () => true)
|
|
9
|
+
continue
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof format === 'function') {
|
|
13
|
+
FormatRegistry.Set(name, format)
|
|
14
|
+
continue
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
FormatRegistry.Set(name, (value) => format.test(value))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const fullFormats: Record<
|
|
22
|
+
string,
|
|
23
|
+
RegExp | ((v: any) => boolean) | true
|
|
24
|
+
> = {
|
|
25
|
+
// date: http://tools.ietf.org/html/rfc3339#section-5.6
|
|
26
|
+
date,
|
|
27
|
+
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
|
|
28
|
+
time: getTime(true),
|
|
29
|
+
'date-time': getDateTime(true),
|
|
30
|
+
'iso-time': getTime(),
|
|
31
|
+
'iso-date-time': getDateTime(),
|
|
32
|
+
// duration: https://tools.ietf.org/html/rfc3339#appendix-A
|
|
33
|
+
duration:
|
|
34
|
+
/^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/,
|
|
35
|
+
uri,
|
|
36
|
+
'uri-reference':
|
|
37
|
+
/^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i,
|
|
38
|
+
// uri-template: https://tools.ietf.org/html/rfc6570
|
|
39
|
+
'uri-template':
|
|
40
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex:
|
|
41
|
+
/^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i,
|
|
42
|
+
// For the source: https://gist.github.com/dperini/729294
|
|
43
|
+
// For test cases: https://mathiasbynens.be/demo/url-regex
|
|
44
|
+
url: /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu,
|
|
45
|
+
email:
|
|
46
|
+
/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,
|
|
47
|
+
hostname:
|
|
48
|
+
/^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i,
|
|
49
|
+
// optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html
|
|
50
|
+
ipv4: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/,
|
|
51
|
+
ipv6: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i,
|
|
52
|
+
regex,
|
|
53
|
+
// uuid: http://tools.ietf.org/html/rfc4122
|
|
54
|
+
uuid: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i,
|
|
55
|
+
// JSON-pointer: https://tools.ietf.org/html/rfc6901
|
|
56
|
+
// uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A
|
|
57
|
+
'json-pointer': /^(?:\/(?:[^~/]|~0|~1)*)*$/,
|
|
58
|
+
'json-pointer-uri-fragment':
|
|
59
|
+
/^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i,
|
|
60
|
+
// relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00
|
|
61
|
+
'relative-json-pointer': /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/,
|
|
62
|
+
// the following formats are used by the openapi specification: https://spec.openapis.org/oas/v3.0.0#data-types
|
|
63
|
+
// byte: https://github.com/miguelmota/is-base64
|
|
64
|
+
byte,
|
|
65
|
+
// signed 32 bit integer
|
|
66
|
+
int32: validateInt32,
|
|
67
|
+
// signed 64 bit integer
|
|
68
|
+
int64: validateInt64,
|
|
69
|
+
// C-type float
|
|
70
|
+
float: validateNumber,
|
|
71
|
+
// C-type double
|
|
72
|
+
double: validateNumber,
|
|
73
|
+
// hint to the UI to hide input strings
|
|
74
|
+
password: true,
|
|
75
|
+
// unchecked string payload
|
|
76
|
+
binary: true,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isLeapYear(year: number): boolean {
|
|
80
|
+
// https://tools.ietf.org/html/rfc3339#appendix-C
|
|
81
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
|
|
85
|
+
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
86
|
+
|
|
87
|
+
function date(str: string): boolean {
|
|
88
|
+
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
|
|
89
|
+
const matches: string[] | null = DATE.exec(str)
|
|
90
|
+
if (!matches) return false
|
|
91
|
+
const year: number = +matches[1]
|
|
92
|
+
const month: number = +matches[2]
|
|
93
|
+
const day: number = +matches[3]
|
|
94
|
+
return (
|
|
95
|
+
month >= 1 &&
|
|
96
|
+
month <= 12 &&
|
|
97
|
+
day >= 1 &&
|
|
98
|
+
day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month])
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i
|
|
103
|
+
|
|
104
|
+
function getTime(strictTimeZone?: boolean): (str: string) => boolean {
|
|
105
|
+
return function time(str: string): boolean {
|
|
106
|
+
const matches: string[] | null = TIME.exec(str)
|
|
107
|
+
if (!matches) return false
|
|
108
|
+
const hr: number = +matches[1]
|
|
109
|
+
const min: number = +matches[2]
|
|
110
|
+
const sec: number = +matches[3]
|
|
111
|
+
const tz: string | undefined = matches[4]
|
|
112
|
+
const tzSign: number = matches[5] === '-' ? -1 : 1
|
|
113
|
+
const tzH: number = +(matches[6] || 0)
|
|
114
|
+
const tzM: number = +(matches[7] || 0)
|
|
115
|
+
if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false
|
|
116
|
+
if (hr <= 23 && min <= 59 && sec < 60) return true
|
|
117
|
+
// leap second
|
|
118
|
+
const utcMin = min - tzM * tzSign
|
|
119
|
+
const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0)
|
|
120
|
+
return (
|
|
121
|
+
(utcHr === 23 || utcHr === -1) &&
|
|
122
|
+
(utcMin === 59 || utcMin === -1) &&
|
|
123
|
+
sec < 61
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const DATE_TIME_SEPARATOR = /t|\s/i
|
|
129
|
+
function getDateTime(strictTimeZone?: boolean): (str: string) => boolean {
|
|
130
|
+
const time = getTime(strictTimeZone)
|
|
131
|
+
|
|
132
|
+
return function date_time(str: string): boolean {
|
|
133
|
+
// http://tools.ietf.org/html/rfc3339#section-5.6
|
|
134
|
+
const dateTime: string[] = str.split(DATE_TIME_SEPARATOR)
|
|
135
|
+
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1])
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const NOT_URI_FRAGMENT = /\/|:/
|
|
140
|
+
const URI =
|
|
141
|
+
/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i
|
|
142
|
+
|
|
143
|
+
function uri(str: string): boolean {
|
|
144
|
+
// http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "."
|
|
145
|
+
return NOT_URI_FRAGMENT.test(str) && URI.test(str)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const BYTE =
|
|
149
|
+
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm
|
|
150
|
+
|
|
151
|
+
function byte(str: string): boolean {
|
|
152
|
+
BYTE.lastIndex = 0
|
|
153
|
+
return BYTE.test(str)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const MIN_INT32 = -(2 ** 31)
|
|
157
|
+
const MAX_INT32 = 2 ** 31 - 1
|
|
158
|
+
|
|
159
|
+
function validateInt32(value: number): boolean {
|
|
160
|
+
return Number.isInteger(value) && value <= MAX_INT32 && value >= MIN_INT32
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateInt64(value: number): boolean {
|
|
164
|
+
// JSON and javascript max Int is 2**53, so any int that passes isInteger is valid for Int64
|
|
165
|
+
return Number.isInteger(value)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateNumber(): boolean {
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const Z_ANCHOR = /[^\\]\\Z/
|
|
173
|
+
function regex(str: string): boolean {
|
|
174
|
+
if (Z_ANCHOR.test(str)) return false
|
|
175
|
+
try {
|
|
176
|
+
new RegExp(str)
|
|
177
|
+
return true
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { KindGuard, type TSchema } from '@sinclair/typebox'
|
|
2
|
+
import { NativeEnumKind, type TNativeEnum } from '../schemas/native-enum.ts'
|
|
3
|
+
|
|
4
|
+
export const IsNativeEnum = (
|
|
5
|
+
schema: TSchema,
|
|
6
|
+
): schema is TNativeEnum<Record<string, string>> =>
|
|
7
|
+
KindGuard.IsKindOf(schema, NativeEnumKind)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KindGuard,
|
|
3
|
+
type TNull,
|
|
4
|
+
type TSchema,
|
|
5
|
+
type TUnion,
|
|
6
|
+
} from '@sinclair/typebox'
|
|
7
|
+
|
|
8
|
+
export const IsNullable = (
|
|
9
|
+
schema: TSchema,
|
|
10
|
+
): schema is TUnion<[TSchema, TNull]> =>
|
|
11
|
+
KindGuard.IsUnion(schema) &&
|
|
12
|
+
schema.anyOf.length === 2 &&
|
|
13
|
+
KindGuard.IsNull(schema.anyOf[1]) &&
|
|
14
|
+
KindGuard.IsSchema(schema.anyOf[0])
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { KindGuard, type TSchema } from '@sinclair/typebox'
|
|
2
|
+
|
|
3
|
+
import { ProcedureKind, type TProcedureContract } from '../schemas/procedure.ts'
|
|
4
|
+
|
|
5
|
+
export const IsProcedure = (schema: TSchema): schema is TProcedureContract =>
|
|
6
|
+
KindGuard.IsKindOf(schema, ProcedureKind)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { KindGuard, type TSchema } from '@sinclair/typebox'
|
|
2
|
+
import {
|
|
3
|
+
SubscriptionKind,
|
|
4
|
+
type TSubscriptionContract,
|
|
5
|
+
} from '../schemas/subscription.ts'
|
|
6
|
+
|
|
7
|
+
export const IsSubscription = (
|
|
8
|
+
schema: TSchema,
|
|
9
|
+
): schema is TSubscriptionContract =>
|
|
10
|
+
KindGuard.IsKindOf(schema, SubscriptionKind)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { KindGuard, type TSchema } from '@sinclair/typebox'
|
|
2
|
+
import { type TUnionEnum, UnionEnumKind } from '../schemas/union-enum.ts'
|
|
3
|
+
|
|
4
|
+
export const IsUnionEnum = (
|
|
5
|
+
schema: TSchema,
|
|
6
|
+
): schema is TUnionEnum<(string | number)[]> =>
|
|
7
|
+
KindGuard.IsKindOf(schema, UnionEnumKind)
|
package/src/guards.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { KindGuard } from '@sinclair/typebox'
|
|
2
|
+
import { IsBlob } from './guards/blob.ts'
|
|
3
|
+
import { IsEvent } from './guards/event.ts'
|
|
4
|
+
import { IsNativeEnum } from './guards/native-enum.ts'
|
|
5
|
+
import { IsNullable } from './guards/nullable.ts'
|
|
6
|
+
import { IsProcedure } from './guards/procedure.ts'
|
|
7
|
+
import { IsService } from './guards/service.ts'
|
|
8
|
+
import { IsSubscription } from './guards/subscription.ts'
|
|
9
|
+
import { IsUnionEnum } from './guards/union-enum.ts'
|
|
10
|
+
|
|
11
|
+
export const ContractGuard = {
|
|
12
|
+
...KindGuard,
|
|
13
|
+
IsEvent,
|
|
14
|
+
IsSubscription,
|
|
15
|
+
IsProcedure,
|
|
16
|
+
IsService,
|
|
17
|
+
IsBlob,
|
|
18
|
+
IsNullable,
|
|
19
|
+
IsUnionEnum,
|
|
20
|
+
IsNativeEnum,
|
|
21
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ApiBlob, type ApiBlobInterface } from '@nmtjs/common'
|
|
2
|
+
import {
|
|
3
|
+
Kind,
|
|
4
|
+
type TSchema,
|
|
5
|
+
Type,
|
|
6
|
+
TypeBoxError,
|
|
7
|
+
TypeRegistry,
|
|
8
|
+
} from '@sinclair/typebox/type'
|
|
9
|
+
import { type ContractSchemaOptions, createSchema } from '../utils.ts'
|
|
10
|
+
|
|
11
|
+
export const BlobKind = 'ApiBlob'
|
|
12
|
+
|
|
13
|
+
export type BlobOptions = {
|
|
14
|
+
maxSize?: number
|
|
15
|
+
contentType?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TBlob extends TSchema {
|
|
19
|
+
[Kind]: typeof BlobKind
|
|
20
|
+
type: 'neemata:blob'
|
|
21
|
+
static: ApiBlobInterface
|
|
22
|
+
maxSize?: BlobOptions['maxSize']
|
|
23
|
+
contentType?: BlobOptions['contentType']
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const BlobType = (
|
|
27
|
+
options: BlobOptions = {},
|
|
28
|
+
schemaOptions: ContractSchemaOptions = {} as ContractSchemaOptions,
|
|
29
|
+
) => {
|
|
30
|
+
if (!TypeRegistry.Has(BlobKind)) {
|
|
31
|
+
TypeRegistry.Set(BlobKind, (schema: TBlob, value) => {
|
|
32
|
+
return 'metadata' in (value as any)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Type.Transform(
|
|
37
|
+
createSchema<TBlob>({
|
|
38
|
+
...schemaOptions,
|
|
39
|
+
[Kind]: BlobKind,
|
|
40
|
+
type: 'neemata:blob',
|
|
41
|
+
...options,
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
.Decode((value) => {
|
|
45
|
+
if ('metadata' in value) {
|
|
46
|
+
if (options.maxSize) {
|
|
47
|
+
const size = (value as ApiBlobInterface).metadata.size
|
|
48
|
+
if (size === -1 || size > options.maxSize) {
|
|
49
|
+
throw new TypeBoxError(
|
|
50
|
+
'Blob size unknown or exceeds maximum allowed size',
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return value
|
|
56
|
+
})
|
|
57
|
+
.Encode((value) => value) as unknown as TBlob
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Kind, type TSchema, TypeRegistry } from '@sinclair/typebox/type'
|
|
2
|
+
import { type ContractSchemaOptions, createSchema } from '../utils.ts'
|
|
3
|
+
|
|
4
|
+
export const EventKind = 'NeemataEvent'
|
|
5
|
+
|
|
6
|
+
export interface TEventContract<
|
|
7
|
+
Payload extends TSchema = any,
|
|
8
|
+
Name extends string | undefined = string | undefined,
|
|
9
|
+
ServiceName extends string | undefined = string | undefined,
|
|
10
|
+
SubscriptionName extends string | undefined = string | undefined,
|
|
11
|
+
> extends TSchema {
|
|
12
|
+
[Kind]: typeof EventKind
|
|
13
|
+
type: 'neemata:event'
|
|
14
|
+
static: {
|
|
15
|
+
payload: Payload['static']
|
|
16
|
+
}
|
|
17
|
+
name: Name
|
|
18
|
+
serviceName: ServiceName
|
|
19
|
+
subscriptionName: SubscriptionName
|
|
20
|
+
payload: Payload
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const EventContract = <Payload extends TSchema>(
|
|
24
|
+
payload: Payload,
|
|
25
|
+
schemaOptions: ContractSchemaOptions = {} as ContractSchemaOptions,
|
|
26
|
+
) => {
|
|
27
|
+
if (!TypeRegistry.Has(EventKind)) TypeRegistry.Set(EventKind, () => true)
|
|
28
|
+
|
|
29
|
+
return createSchema<TEventContract<Payload>>({
|
|
30
|
+
...schemaOptions,
|
|
31
|
+
[Kind]: EventKind,
|
|
32
|
+
type: 'neemata:event',
|
|
33
|
+
payload,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Kind,
|
|
3
|
+
type SchemaOptions,
|
|
4
|
+
type TSchema,
|
|
5
|
+
TypeRegistry,
|
|
6
|
+
} from '@sinclair/typebox/type'
|
|
7
|
+
|
|
8
|
+
export const NativeEnumKind = 'NativeEnum'
|
|
9
|
+
|
|
10
|
+
// -------------------------------------------------------------------------------------
|
|
11
|
+
// TNativeEnum
|
|
12
|
+
// -------------------------------------------------------------------------------------
|
|
13
|
+
export interface TNativeEnum<T extends Record<string, string>> extends TSchema {
|
|
14
|
+
[Kind]: typeof NativeEnumKind
|
|
15
|
+
static: T[keyof T][]
|
|
16
|
+
enum: T[keyof T][]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// -------------------------------------------------------------------------------------
|
|
20
|
+
// NativeEnum
|
|
21
|
+
// -------------------------------------------------------------------------------------
|
|
22
|
+
/** `[Experimental]` Creates a Union type with a `enum` schema representation */
|
|
23
|
+
export function NativeEnum<T extends Record<string, string>>(
|
|
24
|
+
value: T,
|
|
25
|
+
options: SchemaOptions = {},
|
|
26
|
+
) {
|
|
27
|
+
const values = Object.values(value)
|
|
28
|
+
|
|
29
|
+
function NativeEnumCheck(schema: TNativeEnum<T>, value: unknown) {
|
|
30
|
+
return typeof value === 'string' && schema.enum.includes(value as any)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!TypeRegistry.Has(NativeEnumKind))
|
|
34
|
+
TypeRegistry.Set(NativeEnumKind, NativeEnumCheck)
|
|
35
|
+
|
|
36
|
+
return { ...options, [Kind]: NativeEnumKind, enum: values } as TNativeEnum<T>
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Kind, type TSchema, TypeRegistry } from '@sinclair/typebox/type'
|
|
2
|
+
|
|
3
|
+
import { type ContractSchemaOptions, createSchema } from '../utils.ts'
|
|
4
|
+
|
|
5
|
+
export const ProcedureKind = 'NeemataProcedure'
|
|
6
|
+
|
|
7
|
+
export interface TBaseProcedureContract<
|
|
8
|
+
Input extends TSchema = TSchema,
|
|
9
|
+
Output extends TSchema = TSchema,
|
|
10
|
+
Name extends string | undefined = string | undefined,
|
|
11
|
+
ServiceName extends string | undefined = string | undefined,
|
|
12
|
+
Transports extends { [K in string]?: true } | undefined =
|
|
13
|
+
| { [K in string]?: true }
|
|
14
|
+
| undefined,
|
|
15
|
+
> extends TSchema {
|
|
16
|
+
name: Name
|
|
17
|
+
serviceName: ServiceName
|
|
18
|
+
transports: Transports
|
|
19
|
+
input: Input
|
|
20
|
+
output: Output
|
|
21
|
+
timeout?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TProcedureContract<
|
|
25
|
+
Input extends TSchema = TSchema,
|
|
26
|
+
Output extends TSchema = TSchema,
|
|
27
|
+
Name extends string | undefined = string | undefined,
|
|
28
|
+
ServiceName extends string | undefined = string | undefined,
|
|
29
|
+
Transports extends { [K in string]?: true } | undefined =
|
|
30
|
+
| { [K in string]?: true }
|
|
31
|
+
| undefined,
|
|
32
|
+
> extends TBaseProcedureContract<Input, Output, Name, ServiceName, Transports> {
|
|
33
|
+
[Kind]: typeof ProcedureKind
|
|
34
|
+
type: 'neemata:procedure'
|
|
35
|
+
static: {
|
|
36
|
+
input: Input['static']
|
|
37
|
+
output: Output['static']
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ProcedureContract = <
|
|
42
|
+
Input extends TSchema,
|
|
43
|
+
Output extends TSchema,
|
|
44
|
+
>(
|
|
45
|
+
input: Input,
|
|
46
|
+
output: Output,
|
|
47
|
+
timeout?: number,
|
|
48
|
+
schemaOptions: ContractSchemaOptions = {} as ContractSchemaOptions,
|
|
49
|
+
) => {
|
|
50
|
+
if (!TypeRegistry.Has(ProcedureKind))
|
|
51
|
+
TypeRegistry.Set(ProcedureKind, () => true)
|
|
52
|
+
|
|
53
|
+
return createSchema<TProcedureContract<Input, Output>>({
|
|
54
|
+
...schemaOptions,
|
|
55
|
+
[Kind]: ProcedureKind,
|
|
56
|
+
type: 'neemata:procedure',
|
|
57
|
+
input,
|
|
58
|
+
output,
|
|
59
|
+
timeout,
|
|
60
|
+
})
|
|
61
|
+
}
|