UNPKG

@atproto/lexicon

Version:

atproto Lexicon schema language library

348 lines 10 kB
import { z } from 'zod'; import { validateLanguage } from '@atproto/common-web'; import { isValidNsid } from '@atproto/syntax'; import { requiredPropertiesRefinement } from './util.js'; export const languageSchema = z .string() .refine(validateLanguage, 'Invalid BCP47 language tag'); export const lexLang = z.record(languageSchema, z.string().optional()); // primitives // = export const lexBoolean = z.object({ type: z.literal('boolean'), description: z.string().optional(), default: z.boolean().optional(), const: z.boolean().optional(), }); export const lexInteger = z.object({ type: z.literal('integer'), description: z.string().optional(), default: z.number().int().optional(), minimum: z.number().int().optional(), maximum: z.number().int().optional(), enum: z.number().int().array().optional(), const: z.number().int().optional(), }); export const lexStringFormat = z.enum([ 'datetime', 'uri', 'at-uri', 'did', 'handle', 'at-identifier', 'nsid', 'cid', 'language', 'tid', 'record-key', ]); export const lexString = z.object({ type: z.literal('string'), format: lexStringFormat.optional(), description: z.string().optional(), default: z.string().optional(), minLength: z.number().int().optional(), maxLength: z.number().int().optional(), minGraphemes: z.number().int().optional(), maxGraphemes: z.number().int().optional(), enum: z.string().array().optional(), const: z.string().optional(), knownValues: z.string().array().optional(), }); export const lexUnknown = z.object({ type: z.literal('unknown'), description: z.string().optional(), }); export const lexPrimitive = z.discriminatedUnion('type', [ lexBoolean, lexInteger, lexString, lexUnknown, ]); // ipld types // = export const lexBytes = z.object({ type: z.literal('bytes'), description: z.string().optional(), maxLength: z.number().optional(), minLength: z.number().optional(), }); export const lexCidLink = z.object({ type: z.literal('cid-link'), description: z.string().optional(), }); export const lexIpldType = z.discriminatedUnion('type', [lexBytes, lexCidLink]); // references // = export const lexRef = z.object({ type: z.literal('ref'), description: z.string().optional(), ref: z.string(), }); export const lexRefUnion = z.object({ type: z.literal('union'), description: z.string().optional(), refs: z.string().array(), closed: z.boolean().optional(), }); export const lexRefVariant = z.discriminatedUnion('type', [lexRef, lexRefUnion]); // blobs // = export const lexBlob = z.object({ type: z.literal('blob'), description: z.string().optional(), accept: z.string().array().optional(), maxSize: z.number().optional(), }); // complex types // = export const lexArray = z.object({ type: z.literal('array'), description: z.string().optional(), items: z.discriminatedUnion('type', [ // lexPrimitive lexBoolean, lexInteger, lexString, lexUnknown, // lexIpldType lexBytes, lexCidLink, // lexRefVariant lexRef, lexRefUnion, // other lexBlob, ]), minLength: z.number().int().optional(), maxLength: z.number().int().optional(), }); export const lexPrimitiveArray = lexArray.merge(z.object({ items: lexPrimitive, })); export const lexToken = z.object({ type: z.literal('token'), description: z.string().optional(), }); export const lexObject = z .object({ type: z.literal('object'), description: z.string().optional(), required: z.string().array().optional(), nullable: z.string().array().optional(), properties: z.record(z.string(), z.discriminatedUnion('type', [ lexArray, // lexPrimitive lexBoolean, lexInteger, lexString, lexUnknown, // lexIpldType lexBytes, lexCidLink, // lexRefVariant lexRef, lexRefUnion, // other lexBlob, ])), }) .superRefine(requiredPropertiesRefinement); // permissions // = const lexPermission = z.intersection(z.object({ type: z.literal('permission'), resource: z.string().nonempty(), }), z.record(z.string(), z .union([ z.array(z.union([z.string(), z.number().int(), z.boolean()])), z.boolean(), z.number().int(), z.string(), ]) .optional())); export const lexPermissionSet = z.object({ type: z.literal('permission-set'), description: z.string().optional(), title: z.string().optional(), 'title:lang': lexLang.optional(), detail: z.string().optional(), 'detail:lang': lexLang.optional(), permissions: z.array(lexPermission), }); // xrpc // = export const lexXrpcParameters = z .object({ type: z.literal('params'), description: z.string().optional(), required: z.string().array().optional(), properties: z.record(z.string(), z.discriminatedUnion('type', [ lexPrimitiveArray, // lexPrimitive lexBoolean, lexInteger, lexString, lexUnknown, ])), }) .superRefine(requiredPropertiesRefinement); export const lexXrpcBody = z.object({ description: z.string().optional(), encoding: z.string(), // @NOTE using discriminatedUnion with a refined schema requires zod >= 4 schema: z.union([lexRefVariant, lexObject]).optional(), }); export const lexXrpcError = z.object({ name: z.string(), description: z.string().optional(), }); export const lexXrpcQuery = z.object({ type: z.literal('query'), description: z.string().optional(), parameters: lexXrpcParameters.optional(), output: lexXrpcBody.optional(), errors: lexXrpcError.array().optional(), }); export const lexXrpcProcedure = z.object({ type: z.literal('procedure'), description: z.string().optional(), parameters: lexXrpcParameters.optional(), input: lexXrpcBody.optional(), output: lexXrpcBody.optional(), errors: lexXrpcError.array().optional(), }); export const lexXrpcSubscription = z.object({ type: z.literal('subscription'), description: z.string().optional(), parameters: lexXrpcParameters.optional(), message: z.object({ description: z.string().optional(), schema: lexRefUnion, }), errors: lexXrpcError.array().optional(), }); // database // = export const lexRecord = z.object({ type: z.literal('record'), description: z.string().optional(), key: z.string().optional(), record: lexObject, }); // core // = // We need to use `z.custom` here because // lexXrpcProperty and lexObject are refined // `z.union` would work, but it's too slow // see #915 for details export const lexUserType = z.custom((val) => { if (!val || typeof val !== 'object') { return; } if (val['type'] === undefined) { return; } switch (val['type']) { case 'record': return lexRecord.parse(val); case 'permission-set': return lexPermissionSet.parse(val); case 'query': return lexXrpcQuery.parse(val); case 'procedure': return lexXrpcProcedure.parse(val); case 'subscription': return lexXrpcSubscription.parse(val); case 'blob': return lexBlob.parse(val); case 'array': return lexArray.parse(val); case 'token': return lexToken.parse(val); case 'object': return lexObject.parse(val); case 'boolean': return lexBoolean.parse(val); case 'integer': return lexInteger.parse(val); case 'string': return lexString.parse(val); case 'bytes': return lexBytes.parse(val); case 'cid-link': return lexCidLink.parse(val); case 'unknown': return lexUnknown.parse(val); } }, (val) => { if (!val || typeof val !== 'object') { return { message: 'Must be an object', fatal: true, }; } if (val['type'] === undefined) { return { message: 'Must have a type', fatal: true, }; } if (typeof val['type'] !== 'string') { return { message: 'Type property must be a string', fatal: true, }; } return { message: `Invalid type: ${val['type']} must be one of: record, query, procedure, subscription, blob, array, token, object, boolean, integer, string, bytes, cid-link, unknown`, fatal: true, }; }); export const lexiconDoc = z .object({ lexicon: z.literal(1), id: z.string().refine(isValidNsid, { message: 'Must be a valid NSID', }), revision: z.number().optional(), description: z.string().optional(), defs: z.record(z.string(), lexUserType), }) .refine((doc) => { for (const [defId, def] of Object.entries(doc.defs)) { if (defId !== 'main' && (def.type === 'record' || def.type === 'permission-set' || def.type === 'procedure' || def.type === 'query' || def.type === 'subscription')) { return false; } } return true; }, { message: `Records, permission sets, procedures, queries, and subscriptions must be the main definition.`, }); // helpers // = export function isValidLexiconDoc(v) { return lexiconDoc.safeParse(v).success; } export function isObj(v) { return v != null && typeof v === 'object'; } export function isDiscriminatedObject(v) { return isObj(v) && '$type' in v && typeof v.$type === 'string'; } export function parseLexiconDoc(v) { lexiconDoc.parse(v); return v; } export class ValidationError extends Error { } export class InvalidLexiconError extends Error { } export class LexiconDefNotFoundError extends Error { } //# sourceMappingURL=types.js.map