mirror of
https://github.com/versia-pub/api.git
synced 2025-12-06 16:38:20 +01:00
feat(federation): ✨ Add new RequestParserHandler to less verbosely handle body parsing
This commit is contained in:
parent
8860d09eb4
commit
09a9f0bbf5
|
|
@ -321,9 +321,7 @@ export class SignatureConstructor {
|
||||||
}\n` +
|
}\n` +
|
||||||
`host: ${url.host}\n` +
|
`host: ${url.host}\n` +
|
||||||
`date: ${finalDate}\n` +
|
`date: ${finalDate}\n` +
|
||||||
`digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString(
|
`digest: SHA-256=${Buffer.from(digest).toString("base64")}\n`;
|
||||||
"base64",
|
|
||||||
)}\n`;
|
|
||||||
|
|
||||||
const signature = await crypto.subtle.sign(
|
const signature = await crypto.subtle.sign(
|
||||||
"Ed25519",
|
"Ed25519",
|
||||||
|
|
|
||||||
95
federation/http/index.test.ts
Normal file
95
federation/http/index.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { beforeEach, describe, expect, jest, test } from "bun:test";
|
||||||
|
import { RequestParserHandler } from "../http/index.ts";
|
||||||
|
import { EntityValidator } from "../validator/index.ts";
|
||||||
|
|
||||||
|
// Pulled from social.lysand.org
|
||||||
|
const validUser = {
|
||||||
|
id: "018eb863-753f-76ff-83d6-fd590de7740a",
|
||||||
|
type: "User",
|
||||||
|
uri: "https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a",
|
||||||
|
bio: {
|
||||||
|
"text/html": {
|
||||||
|
content: "<p>Hey</p>\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created_at: "2024-04-07T11:48:29.623Z",
|
||||||
|
dislikes:
|
||||||
|
"https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a/dislikes",
|
||||||
|
featured:
|
||||||
|
"https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a/featured",
|
||||||
|
likes: "https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a/likes",
|
||||||
|
followers:
|
||||||
|
"https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a/followers",
|
||||||
|
following:
|
||||||
|
"https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a/following",
|
||||||
|
inbox: "https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a/inbox",
|
||||||
|
outbox: "https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a/outbox",
|
||||||
|
indexable: false,
|
||||||
|
username: "jessew",
|
||||||
|
display_name: "Jesse Wierzbinski",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: { "text/html": { content: "<p>Identity</p>\n" } },
|
||||||
|
value: {
|
||||||
|
"text/html": {
|
||||||
|
content:
|
||||||
|
'<p><a href="https://keyoxide.org/aspe:keyoxide.org:NKLLPWPV7P35NEU7JP4K4ID4CA">https://keyoxide.org/aspe:keyoxide.org:NKLLPWPV7P35NEU7JP4K4ID4CA</a></p>\n',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
public_key: {
|
||||||
|
actor: "https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a",
|
||||||
|
public_key: "XXXXXXXX",
|
||||||
|
},
|
||||||
|
extensions: { "org.lysand:custom_emojis": { emojis: [] } },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("LysandRequestHandler", () => {
|
||||||
|
let validator: EntityValidator;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
validator = new EntityValidator();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseBody with valid User", async () => {
|
||||||
|
const handler = new RequestParserHandler(validUser, validator);
|
||||||
|
|
||||||
|
const noteCallback = jest.fn();
|
||||||
|
await handler.parseBody({ user: noteCallback });
|
||||||
|
|
||||||
|
expect(noteCallback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Throw on invalid Note", async () => {
|
||||||
|
const handler = new RequestParserHandler(
|
||||||
|
{
|
||||||
|
type: "Note",
|
||||||
|
body: "bad",
|
||||||
|
},
|
||||||
|
validator,
|
||||||
|
);
|
||||||
|
|
||||||
|
const noteCallback = jest.fn();
|
||||||
|
await expect(
|
||||||
|
handler.parseBody({ note: noteCallback }),
|
||||||
|
).rejects.toThrow();
|
||||||
|
expect(noteCallback).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Throw on incorrect body type property", async () => {
|
||||||
|
const handler = new RequestParserHandler(
|
||||||
|
{
|
||||||
|
type: "DoesntExist",
|
||||||
|
body: "bad",
|
||||||
|
},
|
||||||
|
validator,
|
||||||
|
);
|
||||||
|
|
||||||
|
const noteCallback = jest.fn();
|
||||||
|
await expect(
|
||||||
|
handler.parseBody({ note: noteCallback }),
|
||||||
|
).rejects.toThrow();
|
||||||
|
expect(noteCallback).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
129
federation/http/index.ts
Normal file
129
federation/http/index.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
import type { EntityValidator } from "../validator/index";
|
||||||
|
|
||||||
|
type MaybePromise<T> = T | Promise<T>;
|
||||||
|
|
||||||
|
type ParserCallbacks = {
|
||||||
|
note: (note: typeof EntityValidator.$Note) => MaybePromise<void>;
|
||||||
|
follow: (follow: typeof EntityValidator.$Follow) => MaybePromise<void>;
|
||||||
|
followAccept: (
|
||||||
|
followAccept: typeof EntityValidator.$FollowAccept,
|
||||||
|
) => MaybePromise<void>;
|
||||||
|
followReject: (
|
||||||
|
followReject: typeof EntityValidator.$FollowReject,
|
||||||
|
) => MaybePromise<void>;
|
||||||
|
user: (user: typeof EntityValidator.$User) => MaybePromise<void>;
|
||||||
|
like: (like: typeof EntityValidator.$Like) => MaybePromise<void>;
|
||||||
|
dislike: (dislike: typeof EntityValidator.$Dislike) => MaybePromise<void>;
|
||||||
|
undo: (undo: typeof EntityValidator.$Undo) => MaybePromise<void>;
|
||||||
|
serverMetadata: (
|
||||||
|
serverMetadata: typeof EntityValidator.$ServerMetadata,
|
||||||
|
) => MaybePromise<void>;
|
||||||
|
extension: (
|
||||||
|
extension: typeof EntityValidator.$Extension,
|
||||||
|
) => MaybePromise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RequestParserHandler {
|
||||||
|
constructor(
|
||||||
|
private readonly body: Record<
|
||||||
|
string,
|
||||||
|
string | number | object | boolean | null
|
||||||
|
>,
|
||||||
|
private readonly validator: EntityValidator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the body of the request and call the appropriate callback.
|
||||||
|
* @param callbacks The callbacks to call when a specific entity is found.
|
||||||
|
* @returns A promise that resolves when the body has been parsed, and the callbacks have finished executing.
|
||||||
|
*/
|
||||||
|
public async parseBody(callbacks: Partial<ParserCallbacks>): Promise<void> {
|
||||||
|
if (!this.body.type) throw new Error("Missing type field in body");
|
||||||
|
|
||||||
|
switch (this.body.type) {
|
||||||
|
case "Note": {
|
||||||
|
const note = await this.validator.Note(this.body);
|
||||||
|
|
||||||
|
if (callbacks.note) await callbacks.note(note);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Follow": {
|
||||||
|
const follow = await this.validator.Follow(this.body);
|
||||||
|
|
||||||
|
if (callbacks.follow) await callbacks.follow(follow);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "FollowAccept": {
|
||||||
|
const followAccept = await this.validator.FollowAccept(
|
||||||
|
this.body,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (callbacks.followAccept)
|
||||||
|
await callbacks.followAccept(followAccept);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "FollowReject": {
|
||||||
|
const followReject = await this.validator.FollowReject(
|
||||||
|
this.body,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (callbacks.followReject)
|
||||||
|
await callbacks.followReject(followReject);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "User": {
|
||||||
|
const user = await this.validator.User(this.body);
|
||||||
|
|
||||||
|
if (callbacks.user) await callbacks.user(user);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Like": {
|
||||||
|
const like = await this.validator.Like(this.body);
|
||||||
|
|
||||||
|
if (callbacks.like) await callbacks.like(like);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Dislike": {
|
||||||
|
const dislike = await this.validator.Dislike(this.body);
|
||||||
|
|
||||||
|
if (callbacks.dislike) await callbacks.dislike(dislike);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Undo": {
|
||||||
|
const undo = await this.validator.Undo(this.body);
|
||||||
|
|
||||||
|
if (callbacks.undo) await callbacks.undo(undo);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ServerMetadata": {
|
||||||
|
const serverMetadata = await this.validator.ServerMetadata(
|
||||||
|
this.body,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (callbacks.serverMetadata)
|
||||||
|
await callbacks.serverMetadata(serverMetadata);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Extension": {
|
||||||
|
const extension = await this.validator.Extension(this.body);
|
||||||
|
|
||||||
|
if (callbacks.extension) await callbacks.extension(extension);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Invalid type field in body: ${this.body.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,323 +5,15 @@
|
||||||
* @see module:federation/schemas/base
|
* @see module:federation/schemas/base
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { z } from "zod";
|
import type { ValidationError } from "zod-validation-error";
|
||||||
import { type ValidationError, fromError } from "zod-validation-error";
|
|
||||||
import { SignatureConstructor, SignatureValidator } from "./cryptography";
|
import { SignatureConstructor, SignatureValidator } from "./cryptography";
|
||||||
import {
|
import { RequestParserHandler } from "./http";
|
||||||
ActionSchema,
|
import { EntityValidator } from "./validator";
|
||||||
ActorPublicKeyDataSchema,
|
|
||||||
ContentFormatSchema,
|
|
||||||
CustomEmojiExtension,
|
|
||||||
DislikeSchema,
|
|
||||||
EntitySchema,
|
|
||||||
ExtensionPropertySchema,
|
|
||||||
ExtensionSchema,
|
|
||||||
FollowAcceptSchema,
|
|
||||||
FollowRejectSchema,
|
|
||||||
FollowSchema,
|
|
||||||
LikeSchema,
|
|
||||||
NoteSchema,
|
|
||||||
PatchSchema,
|
|
||||||
PublicationSchema,
|
|
||||||
ReportSchema,
|
|
||||||
ServerMetadataSchema,
|
|
||||||
UndoSchema,
|
|
||||||
UserSchema,
|
|
||||||
VanityExtensionSchema,
|
|
||||||
VisibilitySchema,
|
|
||||||
} from "./schemas/base";
|
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Used only as a base type
|
|
||||||
type AnyZod = z.ZodType<any, any, any>;
|
|
||||||
|
|
||||||
type InferType<T extends AnyZod> = z.infer<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates entities against their respective schemas.
|
|
||||||
* @module federation/validator
|
|
||||||
* @see module:federation/schemas/base
|
|
||||||
* @example
|
|
||||||
* import { EntityValidator, type ValidationError } from "@lysand-org/federation";
|
|
||||||
* const validator = new EntityValidator();
|
|
||||||
*
|
|
||||||
* // Will throw a special ValidationError with a human-friendly error message
|
|
||||||
* // and a machine-friendly error object if the data is invalid.
|
|
||||||
* const note = await validator.Note({
|
|
||||||
* type: "Note",
|
|
||||||
* content: "Hello, world!",
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* try {
|
|
||||||
* await validator.Note({
|
|
||||||
* type: "Note",
|
|
||||||
* content: 123,
|
|
||||||
* });
|
|
||||||
* } catch (error) {
|
|
||||||
* sendUser((error as ValidationError).toString());
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // Types are also included for TypeScript users that don't use the extracted ones
|
|
||||||
* const noteObject: typeof EntityValidator.$Note = {
|
|
||||||
* type: "Note",
|
|
||||||
* // ...
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
class EntityValidator {
|
|
||||||
private async validate<T extends AnyZod>(
|
|
||||||
schema: T,
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<T>> {
|
|
||||||
try {
|
|
||||||
return (await schema.parseAsync(data)) as InferType<T>;
|
|
||||||
} catch (error) {
|
|
||||||
throw fromError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare static $Note: InferType<typeof NoteSchema>;
|
|
||||||
declare static $Patch: InferType<typeof PatchSchema>;
|
|
||||||
declare static $ActorPublicKeyData: InferType<
|
|
||||||
typeof ActorPublicKeyDataSchema
|
|
||||||
>;
|
|
||||||
declare static $ExtensionProperty: InferType<
|
|
||||||
typeof ExtensionPropertySchema
|
|
||||||
>;
|
|
||||||
declare static $VanityExtension: InferType<typeof VanityExtensionSchema>;
|
|
||||||
declare static $User: InferType<typeof UserSchema>;
|
|
||||||
declare static $Action: InferType<typeof ActionSchema>;
|
|
||||||
declare static $Like: InferType<typeof LikeSchema>;
|
|
||||||
declare static $Undo: InferType<typeof UndoSchema>;
|
|
||||||
declare static $Dislike: InferType<typeof DislikeSchema>;
|
|
||||||
declare static $Follow: InferType<typeof FollowSchema>;
|
|
||||||
declare static $FollowAccept: InferType<typeof FollowAcceptSchema>;
|
|
||||||
declare static $FollowReject: InferType<typeof FollowRejectSchema>;
|
|
||||||
declare static $Extension: InferType<typeof ExtensionSchema>;
|
|
||||||
declare static $Report: InferType<typeof ReportSchema>;
|
|
||||||
declare static $ServerMetadata: InferType<typeof ServerMetadataSchema>;
|
|
||||||
declare static $ContentFormat: InferType<typeof ContentFormatSchema>;
|
|
||||||
declare static $CustomEmojiExtension: InferType<
|
|
||||||
typeof CustomEmojiExtension
|
|
||||||
>;
|
|
||||||
declare static $Visibility: InferType<typeof VisibilitySchema>;
|
|
||||||
declare static $Publication: InferType<typeof PublicationSchema>;
|
|
||||||
declare static $Entity: InferType<typeof EntitySchema>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a Note entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Note(data: unknown): Promise<InferType<typeof NoteSchema>> {
|
|
||||||
return this.validate(NoteSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a Patch entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Patch(data: unknown): Promise<InferType<typeof PatchSchema>> {
|
|
||||||
return this.validate(PatchSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates an ActorPublicKeyData entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public ActorPublicKeyData(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof ActorPublicKeyDataSchema>> {
|
|
||||||
return this.validate(ActorPublicKeyDataSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a VanityExtension entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public VanityExtension(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof VanityExtensionSchema>> {
|
|
||||||
return this.validate(VanityExtensionSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a User entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public User(data: unknown): Promise<InferType<typeof UserSchema>> {
|
|
||||||
return this.validate(UserSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates an Action entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Action(data: unknown): Promise<InferType<typeof ActionSchema>> {
|
|
||||||
return this.validate(ActionSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a Like entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Like(data: unknown): Promise<InferType<typeof LikeSchema>> {
|
|
||||||
return this.validate(LikeSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates an Undo entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Undo(data: unknown): Promise<InferType<typeof UndoSchema>> {
|
|
||||||
return this.validate(UndoSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a Dislike entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Dislike(data: unknown): Promise<InferType<typeof DislikeSchema>> {
|
|
||||||
return this.validate(DislikeSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a Follow entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Follow(data: unknown): Promise<InferType<typeof FollowSchema>> {
|
|
||||||
return this.validate(FollowSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a FollowAccept entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public FollowAccept(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof FollowAcceptSchema>> {
|
|
||||||
return this.validate(FollowAcceptSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a FollowReject entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public FollowReject(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof FollowRejectSchema>> {
|
|
||||||
return this.validate(FollowRejectSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates an Extension entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Extension(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof ExtensionSchema>> {
|
|
||||||
return this.validate(ExtensionSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a Report entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Report(data: unknown): Promise<InferType<typeof ReportSchema>> {
|
|
||||||
return this.validate(ReportSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a ServerMetadata entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public ServerMetadata(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof ServerMetadataSchema>> {
|
|
||||||
return this.validate(ServerMetadataSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a ContentFormat entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public ContentFormat(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof ContentFormatSchema>> {
|
|
||||||
return this.validate(ContentFormatSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a CustomEmojiExtension entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public CustomEmojiExtension(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof CustomEmojiExtension>> {
|
|
||||||
return this.validate(CustomEmojiExtension, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a Visibility entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Visibility(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof VisibilitySchema>> {
|
|
||||||
return this.validate(VisibilitySchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a Publication entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Publication(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof PublicationSchema>> {
|
|
||||||
return this.validate(PublicationSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates an Entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public Entity(data: unknown): Promise<InferType<typeof EntitySchema>> {
|
|
||||||
return this.validate(EntitySchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates an ExtensionProperty.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public ExtensionProperty(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<InferType<typeof ExtensionPropertySchema>> {
|
|
||||||
return this.validate(ExtensionPropertySchema, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
EntityValidator,
|
EntityValidator,
|
||||||
type ValidationError,
|
type ValidationError,
|
||||||
SignatureConstructor,
|
SignatureConstructor,
|
||||||
SignatureValidator,
|
SignatureValidator,
|
||||||
|
RequestParserHandler,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import type { ValidationError } from "zod-validation-error";
|
|
||||||
import { EntityValidator } from "../index";
|
import { EntityValidator } from "../index";
|
||||||
|
|
||||||
describe("Package testing", () => {
|
describe("Package testing", () => {
|
||||||
|
|
|
||||||
312
federation/validator/index.ts
Normal file
312
federation/validator/index.ts
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
import type { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import {
|
||||||
|
ActionSchema,
|
||||||
|
ActorPublicKeyDataSchema,
|
||||||
|
ContentFormatSchema,
|
||||||
|
CustomEmojiExtension,
|
||||||
|
DislikeSchema,
|
||||||
|
EntitySchema,
|
||||||
|
ExtensionPropertySchema,
|
||||||
|
ExtensionSchema,
|
||||||
|
FollowAcceptSchema,
|
||||||
|
FollowRejectSchema,
|
||||||
|
FollowSchema,
|
||||||
|
LikeSchema,
|
||||||
|
NoteSchema,
|
||||||
|
PatchSchema,
|
||||||
|
PublicationSchema,
|
||||||
|
ReportSchema,
|
||||||
|
ServerMetadataSchema,
|
||||||
|
UndoSchema,
|
||||||
|
UserSchema,
|
||||||
|
VanityExtensionSchema,
|
||||||
|
VisibilitySchema,
|
||||||
|
} from "../schemas/base";
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Used only as a base type
|
||||||
|
type AnyZod = z.ZodType<any, any, any>;
|
||||||
|
|
||||||
|
type InferType<T extends AnyZod> = z.infer<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates entities against their respective schemas.
|
||||||
|
* @module federation/validator
|
||||||
|
* @see module:federation/schemas/base
|
||||||
|
* @example
|
||||||
|
* import { EntityValidator, type ValidationError } from "@lysand-org/federation";
|
||||||
|
* const validator = new EntityValidator();
|
||||||
|
*
|
||||||
|
* // Will throw a special ValidationError with a human-friendly error message
|
||||||
|
* // and a machine-friendly error object if the data is invalid.
|
||||||
|
* const note = await validator.Note({
|
||||||
|
* type: "Note",
|
||||||
|
* content: "Hello, world!",
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* try {
|
||||||
|
* await validator.Note({
|
||||||
|
* type: "Note",
|
||||||
|
* content: 123,
|
||||||
|
* });
|
||||||
|
* } catch (error) {
|
||||||
|
* sendUser((error as ValidationError).toString());
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Types are also included for TypeScript users that don't use the extracted ones
|
||||||
|
* const noteObject: typeof EntityValidator.$Note = {
|
||||||
|
* type: "Note",
|
||||||
|
* // ...
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export class EntityValidator {
|
||||||
|
private async validate<T extends AnyZod>(
|
||||||
|
schema: T,
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<T>> {
|
||||||
|
try {
|
||||||
|
return (await schema.parseAsync(data)) as InferType<T>;
|
||||||
|
} catch (error) {
|
||||||
|
throw fromError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare static $Note: InferType<typeof NoteSchema>;
|
||||||
|
declare static $Patch: InferType<typeof PatchSchema>;
|
||||||
|
declare static $ActorPublicKeyData: InferType<
|
||||||
|
typeof ActorPublicKeyDataSchema
|
||||||
|
>;
|
||||||
|
declare static $ExtensionProperty: InferType<
|
||||||
|
typeof ExtensionPropertySchema
|
||||||
|
>;
|
||||||
|
declare static $VanityExtension: InferType<typeof VanityExtensionSchema>;
|
||||||
|
declare static $User: InferType<typeof UserSchema>;
|
||||||
|
declare static $Action: InferType<typeof ActionSchema>;
|
||||||
|
declare static $Like: InferType<typeof LikeSchema>;
|
||||||
|
declare static $Undo: InferType<typeof UndoSchema>;
|
||||||
|
declare static $Dislike: InferType<typeof DislikeSchema>;
|
||||||
|
declare static $Follow: InferType<typeof FollowSchema>;
|
||||||
|
declare static $FollowAccept: InferType<typeof FollowAcceptSchema>;
|
||||||
|
declare static $FollowReject: InferType<typeof FollowRejectSchema>;
|
||||||
|
declare static $Extension: InferType<typeof ExtensionSchema>;
|
||||||
|
declare static $Report: InferType<typeof ReportSchema>;
|
||||||
|
declare static $ServerMetadata: InferType<typeof ServerMetadataSchema>;
|
||||||
|
declare static $ContentFormat: InferType<typeof ContentFormatSchema>;
|
||||||
|
declare static $CustomEmojiExtension: InferType<
|
||||||
|
typeof CustomEmojiExtension
|
||||||
|
>;
|
||||||
|
declare static $Visibility: InferType<typeof VisibilitySchema>;
|
||||||
|
declare static $Publication: InferType<typeof PublicationSchema>;
|
||||||
|
declare static $Entity: InferType<typeof EntitySchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a Note entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Note(data: unknown): Promise<InferType<typeof NoteSchema>> {
|
||||||
|
return this.validate(NoteSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a Patch entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Patch(data: unknown): Promise<InferType<typeof PatchSchema>> {
|
||||||
|
return this.validate(PatchSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an ActorPublicKeyData entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public ActorPublicKeyData(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof ActorPublicKeyDataSchema>> {
|
||||||
|
return this.validate(ActorPublicKeyDataSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a VanityExtension entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public VanityExtension(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof VanityExtensionSchema>> {
|
||||||
|
return this.validate(VanityExtensionSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a User entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public User(data: unknown): Promise<InferType<typeof UserSchema>> {
|
||||||
|
return this.validate(UserSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an Action entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Action(data: unknown): Promise<InferType<typeof ActionSchema>> {
|
||||||
|
return this.validate(ActionSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a Like entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Like(data: unknown): Promise<InferType<typeof LikeSchema>> {
|
||||||
|
return this.validate(LikeSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an Undo entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Undo(data: unknown): Promise<InferType<typeof UndoSchema>> {
|
||||||
|
return this.validate(UndoSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a Dislike entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Dislike(data: unknown): Promise<InferType<typeof DislikeSchema>> {
|
||||||
|
return this.validate(DislikeSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a Follow entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Follow(data: unknown): Promise<InferType<typeof FollowSchema>> {
|
||||||
|
return this.validate(FollowSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a FollowAccept entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public FollowAccept(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof FollowAcceptSchema>> {
|
||||||
|
return this.validate(FollowAcceptSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a FollowReject entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public FollowReject(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof FollowRejectSchema>> {
|
||||||
|
return this.validate(FollowRejectSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an Extension entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Extension(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof ExtensionSchema>> {
|
||||||
|
return this.validate(ExtensionSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a Report entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Report(data: unknown): Promise<InferType<typeof ReportSchema>> {
|
||||||
|
return this.validate(ReportSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a ServerMetadata entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public ServerMetadata(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof ServerMetadataSchema>> {
|
||||||
|
return this.validate(ServerMetadataSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a ContentFormat entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public ContentFormat(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof ContentFormatSchema>> {
|
||||||
|
return this.validate(ContentFormatSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a CustomEmojiExtension entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public CustomEmojiExtension(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof CustomEmojiExtension>> {
|
||||||
|
return this.validate(CustomEmojiExtension, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a Visibility entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Visibility(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof VisibilitySchema>> {
|
||||||
|
return this.validate(VisibilitySchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a Publication entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Publication(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof PublicationSchema>> {
|
||||||
|
return this.validate(PublicationSchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an Entity.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public Entity(data: unknown): Promise<InferType<typeof EntitySchema>> {
|
||||||
|
return this.validate(EntitySchema, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an ExtensionProperty.
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns A promise that resolves to the validated data.
|
||||||
|
*/
|
||||||
|
public ExtensionProperty(
|
||||||
|
data: unknown,
|
||||||
|
): Promise<InferType<typeof ExtensionPropertySchema>> {
|
||||||
|
return this.validate(ExtensionPropertySchema, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue