@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.
Files changed (67) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +9 -0
  3. package/dist/compiler.js +50 -0
  4. package/dist/compiler.js.map +1 -0
  5. package/dist/contract.js +24 -0
  6. package/dist/contract.js.map +1 -0
  7. package/dist/formats.js +127 -0
  8. package/dist/formats.js.map +1 -0
  9. package/dist/guards/blob.js +3 -0
  10. package/dist/guards/blob.js.map +1 -0
  11. package/dist/guards/event.js +3 -0
  12. package/dist/guards/event.js.map +1 -0
  13. package/dist/guards/native-enum.js +3 -0
  14. package/dist/guards/native-enum.js.map +1 -0
  15. package/dist/guards/nullable.js +2 -0
  16. package/dist/guards/nullable.js.map +1 -0
  17. package/dist/guards/procedure.js +3 -0
  18. package/dist/guards/procedure.js.map +1 -0
  19. package/dist/guards/service.js +3 -0
  20. package/dist/guards/service.js.map +1 -0
  21. package/dist/guards/subscription.js +3 -0
  22. package/dist/guards/subscription.js.map +1 -0
  23. package/dist/guards/union-enum.js +3 -0
  24. package/dist/guards/union-enum.js.map +1 -0
  25. package/dist/guards.js +20 -0
  26. package/dist/guards.js.map +1 -0
  27. package/dist/schemas/blob.js +26 -0
  28. package/dist/schemas/blob.js.map +1 -0
  29. package/dist/schemas/event.js +12 -0
  30. package/dist/schemas/event.js.map +1 -0
  31. package/dist/schemas/native-enum.js +14 -0
  32. package/dist/schemas/native-enum.js.map +1 -0
  33. package/dist/schemas/nullable.js +5 -0
  34. package/dist/schemas/nullable.js.map +1 -0
  35. package/dist/schemas/procedure.js +14 -0
  36. package/dist/schemas/procedure.js.map +1 -0
  37. package/dist/schemas/service.js +35 -0
  38. package/dist/schemas/service.js.map +1 -0
  39. package/dist/schemas/subscription.js +16 -0
  40. package/dist/schemas/subscription.js.map +1 -0
  41. package/dist/schemas/union-enum.js +13 -0
  42. package/dist/schemas/union-enum.js.map +1 -0
  43. package/dist/utils.js +11 -0
  44. package/dist/utils.js.map +1 -0
  45. package/package.json +41 -0
  46. package/src/compiler.ts +63 -0
  47. package/src/contract.ts +62 -0
  48. package/src/formats.ts +181 -0
  49. package/src/guards/blob.ts +5 -0
  50. package/src/guards/event.ts +6 -0
  51. package/src/guards/native-enum.ts +7 -0
  52. package/src/guards/nullable.ts +14 -0
  53. package/src/guards/procedure.ts +6 -0
  54. package/src/guards/service.ts +6 -0
  55. package/src/guards/subscription.ts +10 -0
  56. package/src/guards/union-enum.ts +7 -0
  57. package/src/guards.ts +21 -0
  58. package/src/schemas/blob.ts +58 -0
  59. package/src/schemas/event.ts +35 -0
  60. package/src/schemas/native-enum.ts +37 -0
  61. package/src/schemas/nullable.ts +4 -0
  62. package/src/schemas/procedure.ts +61 -0
  63. package/src/schemas/service.ts +126 -0
  64. package/src/schemas/subscription.ts +82 -0
  65. package/src/schemas/union-enum.ts +43 -0
  66. package/src/utils.ts +16 -0
  67. 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
+ }
@@ -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
+ }
@@ -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,5 @@
1
+ import { KindGuard, type TSchema } from '@sinclair/typebox'
2
+ import { BlobKind, type TBlob } from '../schemas/blob.ts'
3
+
4
+ export const IsBlob = (schema: TSchema): schema is TBlob =>
5
+ KindGuard.IsKindOf(schema, BlobKind)
@@ -0,0 +1,6 @@
1
+ import { KindGuard, type TSchema } from '@sinclair/typebox'
2
+
3
+ import { EventKind, type TEventContract } from '../schemas/event.ts'
4
+
5
+ export const IsEvent = (schema: TSchema): schema is TEventContract =>
6
+ KindGuard.IsKindOf(schema, EventKind)
@@ -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,6 @@
1
+ import { KindGuard, type TSchema } from '@sinclair/typebox'
2
+
3
+ import { ServiceKind, type TServiceContract } from '../schemas/service.ts'
4
+
5
+ export const IsService = (schema: TSchema): schema is TServiceContract =>
6
+ KindGuard.IsKindOf(schema, ServiceKind)
@@ -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,4 @@
1
+ import { type TSchema, Type } from '@sinclair/typebox/type'
2
+
3
+ export const Nullable = <T extends TSchema>(schema: T) =>
4
+ Type.Union([schema, Type.Null()])
@@ -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
+ }