@atproto/lexicon
Version:
atproto Lexicon schema language library
348 lines • 10 kB
JavaScript
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