Add serializable project commands
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { serializeProjectCommand } from "../../domain/models/project-command.model.js";
|
||||
import type {
|
||||
AppendProjectRevisionInput,
|
||||
AppendedProjectRevision,
|
||||
@@ -40,6 +41,8 @@ export class ProjectRevisionRepository implements ProjectRevisionStore {
|
||||
append(input: AppendProjectRevisionInput): AppendedProjectRevision {
|
||||
this.validateInput(input);
|
||||
|
||||
const forwardPayloadJson = serializeProjectCommand(input.forward);
|
||||
const inversePayloadJson = serializeProjectCommand(input.inverse);
|
||||
const revisionId = crypto.randomUUID();
|
||||
const changeSetId = crypto.randomUUID();
|
||||
const revisionNumber = input.expectedRevision + 1;
|
||||
@@ -86,10 +89,10 @@ export class ProjectRevisionRepository implements ProjectRevisionStore {
|
||||
.values({
|
||||
id: changeSetId,
|
||||
projectRevisionId: revisionId,
|
||||
commandType: input.commandType,
|
||||
commandType: input.forward.type,
|
||||
payloadSchemaVersion: input.forward.schemaVersion,
|
||||
forwardPayloadJson: JSON.stringify(input.forward.data),
|
||||
inversePayloadJson: JSON.stringify(input.inverse.data),
|
||||
forwardPayloadJson,
|
||||
inversePayloadJson,
|
||||
})
|
||||
.run();
|
||||
|
||||
@@ -107,16 +110,11 @@ export class ProjectRevisionRepository implements ProjectRevisionStore {
|
||||
if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) {
|
||||
throw new Error("Expected project revision must be a non-negative integer.");
|
||||
}
|
||||
if (!input.commandType.trim()) {
|
||||
throw new Error("Project revision command type is required.");
|
||||
}
|
||||
if (
|
||||
!Number.isSafeInteger(input.forward.schemaVersion) ||
|
||||
input.forward.schemaVersion < 1 ||
|
||||
input.forward.schemaVersion !== input.inverse.schemaVersion
|
||||
) {
|
||||
throw new Error(
|
||||
"Forward and inverse payloads require the same positive schema version."
|
||||
"Forward and inverse project commands require the same schema version."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
export type ProjectCommandJsonValue =
|
||||
| null
|
||||
| boolean
|
||||
| number
|
||||
| string
|
||||
| ProjectCommandJsonValue[]
|
||||
| { [key: string]: ProjectCommandJsonValue };
|
||||
|
||||
export interface SerializedProjectCommand {
|
||||
schemaVersion: number;
|
||||
type: string;
|
||||
payload: ProjectCommandJsonValue;
|
||||
}
|
||||
|
||||
export function serializeProjectCommand(command: SerializedProjectCommand): string {
|
||||
assertSerializedProjectCommand(command);
|
||||
return JSON.stringify(command);
|
||||
}
|
||||
|
||||
export function deserializeProjectCommand(serialized: string): SerializedProjectCommand {
|
||||
const parsed: unknown = JSON.parse(serialized);
|
||||
assertSerializedProjectCommand(parsed);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function assertSerializedProjectCommand(
|
||||
value: unknown
|
||||
): asserts value is SerializedProjectCommand {
|
||||
if (!isPlainObject(value)) {
|
||||
throw new Error("Project command must be an object.");
|
||||
}
|
||||
if (
|
||||
!Number.isSafeInteger(value.schemaVersion) ||
|
||||
(value.schemaVersion as number) < 1
|
||||
) {
|
||||
throw new Error("Project command schema version must be a positive integer.");
|
||||
}
|
||||
if (typeof value.type !== "string" || !value.type.trim()) {
|
||||
throw new Error("Project command type is required.");
|
||||
}
|
||||
assertJsonValue(value.payload, new WeakSet<object>());
|
||||
}
|
||||
|
||||
function assertJsonValue(value: unknown, parents: WeakSet<object>): void {
|
||||
if (
|
||||
value === null ||
|
||||
typeof value === "string" ||
|
||||
typeof value === "boolean"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
if (!Number.isFinite(value)) {
|
||||
throw new Error("Project command numbers must be finite.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof value !== "object") {
|
||||
throw new Error("Project command payload must contain JSON values only.");
|
||||
}
|
||||
if (parents.has(value)) {
|
||||
throw new Error("Project command payload must not contain cycles.");
|
||||
}
|
||||
|
||||
parents.add(value);
|
||||
if (Array.isArray(value)) {
|
||||
for (const entry of value) {
|
||||
assertJsonValue(entry, parents);
|
||||
}
|
||||
} else {
|
||||
if (!isPlainObject(value)) {
|
||||
throw new Error("Project command payload must contain plain objects only.");
|
||||
}
|
||||
for (const entry of Object.values(value)) {
|
||||
assertJsonValue(entry, parents);
|
||||
}
|
||||
}
|
||||
parents.delete(value);
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
if (value === null || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { SerializedProjectCommand } from "../models/project-command.model.js";
|
||||
|
||||
export type ProjectRevisionSource =
|
||||
| "user"
|
||||
| "undo"
|
||||
@@ -5,28 +7,14 @@ export type ProjectRevisionSource =
|
||||
| "restore"
|
||||
| "migration";
|
||||
|
||||
export type ProjectChangeJsonValue =
|
||||
| null
|
||||
| boolean
|
||||
| number
|
||||
| string
|
||||
| ProjectChangeJsonValue[]
|
||||
| { [key: string]: ProjectChangeJsonValue };
|
||||
|
||||
export interface ProjectChangePayload {
|
||||
schemaVersion: number;
|
||||
data: ProjectChangeJsonValue;
|
||||
}
|
||||
|
||||
export interface AppendProjectRevisionInput {
|
||||
projectId: string;
|
||||
expectedRevision: number;
|
||||
source: ProjectRevisionSource;
|
||||
commandType: string;
|
||||
description?: string;
|
||||
actorId?: string;
|
||||
forward: ProjectChangePayload;
|
||||
inverse: ProjectChangePayload;
|
||||
forward: SerializedProjectCommand;
|
||||
inverse: SerializedProjectCommand;
|
||||
}
|
||||
|
||||
export interface AppendedProjectRevision {
|
||||
|
||||
Reference in New Issue
Block a user