@atproto/lex-document 0.0.2 → 0.0.4

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.
@@ -1,4 +1,5 @@
1
- import { CID, l } from '@atproto/lex-schema'
1
+ import { parseCid } from '@atproto/lex-data'
2
+ import { l } from '@atproto/lex-schema'
2
3
  import { LexiconDocument, lexiconDocumentSchema } from './lexicon-document.js'
3
4
  import { LexiconIterableIndexer } from './lexicon-iterable-indexer.js'
4
5
  import { LexiconSchemaBuilder } from './lexicon-schema-builder.js'
@@ -59,20 +60,26 @@ describe('LexiconSchemaBuilder', () => {
59
60
  type: 'object',
60
61
  required: ['object', 'array', 'boolean', 'integer', 'string'],
61
62
  properties: {
62
- object: { type: 'ref', ref: '#subobject' },
63
+ object: { type: 'ref', ref: '#subObject' },
63
64
  array: { type: 'array', items: { type: 'string' } },
64
65
  boolean: { type: 'boolean' },
65
66
  integer: { type: 'integer' },
66
67
  string: { type: 'string' },
68
+ refToEnumWithDefault: { type: 'ref', ref: '#enumWithDefault' },
67
69
  },
68
70
  },
69
- subobject: {
71
+ subObject: {
70
72
  type: 'object',
71
73
  required: ['boolean'],
72
74
  properties: {
73
75
  boolean: { type: 'boolean' },
74
76
  },
75
77
  },
78
+ enumWithDefault: {
79
+ type: 'string',
80
+ default: 'option3',
81
+ enum: ['option1', 'option2', 'option3'],
82
+ },
76
83
  },
77
84
  }),
78
85
  ])
@@ -90,6 +97,7 @@ describe('LexiconSchemaBuilder', () => {
90
97
  boolean: true,
91
98
  integer: 123,
92
99
  string: 'string',
100
+ refToEnumWithDefault: 'option3',
93
101
  },
94
102
  array: ['one', 'two'],
95
103
  boolean: true,
@@ -100,12 +108,12 @@ describe('LexiconSchemaBuilder', () => {
100
108
  did: 'did:web:example.com',
101
109
  cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
102
110
  bytes: new Uint8Array([0, 1, 2, 3]),
103
- cidLink: CID.parse(
111
+ cidLink: parseCid(
104
112
  'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
105
113
  ),
106
114
  }
107
115
 
108
- expect(schema.validate(value)).toStrictEqual({ success: true, value })
116
+ expect(schema.safeParse(value)).toStrictEqual({ success: true, value })
109
117
  })
110
118
 
111
119
  it('Validates objects correctly', () => {
@@ -122,7 +130,89 @@ describe('LexiconSchemaBuilder', () => {
122
130
  string: 'string',
123
131
  }
124
132
 
125
- expect(schema.validate(value)).toStrictEqual({ success: true, value })
133
+ expect(schema.safeParse(value)).toStrictEqual({
134
+ success: true,
135
+ value: {
136
+ object: { boolean: true },
137
+ array: ['one', 'two'],
138
+ boolean: true,
139
+ integer: 123,
140
+ string: 'string',
141
+ refToEnumWithDefault: 'option3',
142
+ },
143
+ })
144
+ })
145
+
146
+ it('rejects invalid enum values', () => {
147
+ const schema = getSchema(
148
+ 'com.example.kitchenSink#object',
149
+ l.TypedObjectSchema,
150
+ )
151
+
152
+ const value = {
153
+ object: { boolean: true },
154
+ array: ['one', 'two'],
155
+ boolean: true,
156
+ integer: 123,
157
+ string: 'string',
158
+ refToEnumWithDefault: 'invalidOption',
159
+ }
160
+
161
+ expect(schema.safeParse(value)).toMatchObject({
162
+ success: false,
163
+ error: {
164
+ issues: [
165
+ {
166
+ code: 'invalid_value',
167
+ input: 'invalidOption',
168
+ values: ['option1', 'option2', 'option3'],
169
+ },
170
+ ],
171
+ },
172
+ })
173
+ })
174
+
175
+ it('does not apply defaults when allowTransform is false', () => {
176
+ const schema = getSchema(
177
+ 'com.example.kitchenSink#object',
178
+ l.TypedObjectSchema,
179
+ )
180
+
181
+ const value = {
182
+ object: { boolean: true },
183
+ array: ['one', 'two'],
184
+ boolean: true,
185
+ integer: 123,
186
+ string: 'string',
187
+ }
188
+
189
+ expect(schema.safeParse(value, { allowTransform: false })).toStrictEqual({
190
+ success: true,
191
+ value: {
192
+ object: { boolean: true },
193
+ array: ['one', 'two'],
194
+ boolean: true,
195
+ integer: 123,
196
+ string: 'string',
197
+ },
198
+ })
199
+ })
200
+
201
+ it('allows missing optional record fields', () => {
202
+ const schema = getSchema(
203
+ 'com.example.kitchenSink#object',
204
+ l.TypedObjectSchema,
205
+ )
206
+
207
+ expect(
208
+ schema.matches({
209
+ object: { boolean: true },
210
+ array: ['one', 'two'],
211
+ boolean: true,
212
+ integer: 123,
213
+ string: 'string',
214
+ }),
215
+ ).toBe(true)
126
216
  })
127
217
 
128
218
  it('Rejects missing required record fields', () => {
@@ -139,7 +229,7 @@ describe('LexiconSchemaBuilder', () => {
139
229
  string: 'string',
140
230
  }
141
231
 
142
- expect(schema.validate(value)).toMatchObject({
232
+ expect(schema.safeParse(value)).toMatchObject({
143
233
  success: false,
144
234
  error: { issues: [{ code: 'required_key', key: 'array' }] },
145
235
  })
@@ -33,8 +33,8 @@ export class LexiconSchemaBuilder {
33
33
  const ctx = new LexiconSchemaBuilder(indexer)
34
34
  try {
35
35
  const result = await ctx.buildFullRef(fullRef)
36
- if (!(result instanceof l.Validator)) {
37
- throw new Error(`Ref ${fullRef} is not a validator schema type`)
36
+ if (!(result instanceof l.Schema)) {
37
+ throw new Error(`Ref ${fullRef} is not a schema type`)
38
38
  }
39
39
  return result
40
40
  } finally {
@@ -90,8 +90,8 @@ export class LexiconSchemaBuilder {
90
90
 
91
91
  this.#asyncTasks.add(
92
92
  this.buildFullRef(fullRef).then((v) => {
93
- if (!(v instanceof l.Validator)) {
94
- throw new Error(`Only refs to validator schema types are allowed`)
93
+ if (!(v instanceof l.Schema)) {
94
+ throw new Error(`Only refs to schema types are allowed`)
95
95
  }
96
96
  validator = v
97
97
  }),
@@ -167,11 +167,7 @@ export class LexiconSchemaBuilder {
167
167
  case 'token':
168
168
  return l.token(doc.id, hash)
169
169
  case 'record':
170
- return l.record(
171
- def.key ? l.asRecordKey(def.key) : 'any',
172
- doc.id,
173
- this.compileObject(doc, def.record),
174
- )
170
+ return l.record(def.key, doc.id, this.compileObject(doc, def.record))
175
171
  case 'object':
176
172
  return l.typedObject(doc.id, hash, this.compileObject(doc, def))
177
173
  default:
@@ -183,13 +179,48 @@ export class LexiconSchemaBuilder {
183
179
  doc: LexiconDocument,
184
180
  def: LexiconArray | LexiconArrayItems,
185
181
  ): l.Validator<unknown> {
182
+ if (
183
+ 'const' in def &&
184
+ 'enum' in def &&
185
+ def.enum != null &&
186
+ def.const !== undefined &&
187
+ !(def.enum as readonly unknown[]).includes(def.const)
188
+ ) {
189
+ return l.never()
190
+ }
191
+
186
192
  switch (def.type) {
187
- case 'string':
188
- return l.string(def)
189
- case 'integer':
190
- return l.integer(def)
191
- case 'boolean':
192
- return l.boolean(def)
193
+ case 'string': {
194
+ const schema: l.StringSchema = l.string(def)
195
+ if (def.const != null) {
196
+ schema.assert(def.const)
197
+ return l.literal(def.const, def)
198
+ } else if (def.enum != null) {
199
+ for (const v of def.enum) schema.assert(v)
200
+ return l.enum(def.enum, def)
201
+ } else {
202
+ return schema
203
+ }
204
+ }
205
+ case 'integer': {
206
+ const schema: l.IntegerSchema = l.integer(def)
207
+ if (def.const != null) {
208
+ schema.assert(def.const)
209
+ return l.literal(def.const, def)
210
+ } else if (def.enum != null) {
211
+ for (const v of def.enum) schema.assert(v)
212
+ return l.enum(def.enum, def)
213
+ } else {
214
+ return schema
215
+ }
216
+ }
217
+ case 'boolean': {
218
+ if (def.const != null) {
219
+ return l.literal(def.const, def)
220
+ } else {
221
+ return l.boolean(def)
222
+ }
223
+ }
193
224
  case 'blob':
194
225
  return l.blob(def)
195
226
  case 'cid-link':
@@ -232,9 +263,23 @@ export class LexiconSchemaBuilder {
232
263
  const props: Record<string, l.Validator> = {}
233
264
  for (const [key, propDef] of Object.entries(def.properties)) {
234
265
  if (propDef === undefined) continue
235
- props[key] = this.compileLeaf(doc, propDef)
266
+
267
+ const isNullable = def.nullable?.includes(key)
268
+ const isRequired = def.required?.includes(key)
269
+
270
+ let schema = this.compileLeaf(doc, propDef)
271
+
272
+ if (isNullable) {
273
+ schema = l.nullable(schema)
274
+ }
275
+
276
+ if (!isRequired) {
277
+ schema = l.optional(schema)
278
+ }
279
+
280
+ props[key] = schema
236
281
  }
237
- return l.object(props, def)
282
+ return l.object(props)
238
283
  }
239
284
 
240
285
  protected compilePayload(
@@ -273,9 +318,18 @@ export class LexiconSchemaBuilder {
273
318
  const props: Record<string, l.Validator> = {}
274
319
  for (const [key, propDef] of Object.entries(def.properties)) {
275
320
  if (propDef === undefined) continue
276
- props[key] = this.compileLeaf(doc, propDef)
321
+
322
+ const isRequired = def.required?.includes(key)
323
+
324
+ let schema = this.compileLeaf(doc, propDef)
325
+
326
+ if (!isRequired) {
327
+ schema = l.optional(schema)
328
+ }
329
+
330
+ props[key] = schema
277
331
  }
278
- return l.params(props, def)
332
+ return l.params(props)
279
333
  }
280
334
  }
281
335
 
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": ["../../../tsconfig/isomorphic.json"],
3
+ "include": ["./src"],
4
+ "exclude": ["**/*.test.ts"],
5
+ "compilerOptions": {
6
+ "noImplicitAny": true,
7
+ "importHelpers": true,
8
+ "target": "ES2023",
9
+ "rootDir": "./src",
10
+ "outDir": "./dist"
11
+ }
12
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "include": [],
3
+ "references": [
4
+ { "path": "./tsconfig.build.json" },
5
+ { "path": "./tsconfig.tests.json" }
6
+ ]
7
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../../tsconfig/tests.json",
3
+ "include": ["./tests", "./src/**.test.ts"],
4
+ "compilerOptions": {
5
+ "noImplicitAny": true,
6
+ "rootDir": "./",
7
+ "baseUrl": "./"
8
+ }
9
+ }