Skip to content

Commit

Permalink
feat(api-headless-cms): start with record locking
Browse files Browse the repository at this point in the history
  • Loading branch information
brunozoric committed Mar 21, 2024
1 parent 66f9984 commit 4571658
Show file tree
Hide file tree
Showing 21 changed files with 592 additions and 86 deletions.
6 changes: 5 additions & 1 deletion packages/api-headless-cms/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { StorageOperationsCmsModelPlugin } from "~/plugins";
import { createCmsModelFieldConvertersAttachFactory } from "~/utils/converters/valueKeyStorageConverter";
import { createExportCrud } from "~/export";
import { createImportCrud } from "~/export/crud/importing";
import { createLockingMechanismCrud } from "~/lockingMechanism/crud";

const getParameters = async (context: CmsContext): Promise<CmsParametersPluginResponse> => {
const plugins = context.plugins.byType<CmsParametersPlugin>(CmsParametersPlugin.type);
Expand Down Expand Up @@ -118,7 +119,10 @@ export const createContextPlugin = ({ storageOperations }: CrudParams) => {
},
importing: {
...createImportCrud(context)
}
},
locking: createLockingMechanismCrud({
context
})
};

if (!storageOperations.init) {
Expand Down
15 changes: 5 additions & 10 deletions packages/api-headless-cms/src/crud/contentEntry.crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
CmsModel,
CmsStorageEntry,
EntryBeforeListTopicParams,
HeadlessCms,
HeadlessCmsStorageOperations,
OnEntryAfterCreateTopicParams,
OnEntryAfterDeleteMultipleTopicParams,
Expand Down Expand Up @@ -69,16 +68,16 @@ import {
} from "./contentEntry/entryDataFactories";
import { AccessControl } from "./AccessControl/AccessControl";
import {
deleteEntryUseCases,
getEntriesByIdsUseCases,
listEntriesUseCases,
getLatestEntriesByIdsUseCases,
getPublishedEntriesByIdsUseCases,
getRevisionsByEntryIdUseCases,
getRevisionByIdUseCases,
getLatestRevisionByEntryIdUseCases,
getPreviousRevisionByEntryIdUseCases,
getPublishedEntriesByIdsUseCases,
getPublishedRevisionByEntryIdUseCases,
deleteEntryUseCases
getRevisionByIdUseCases,
getRevisionsByEntryIdUseCases,
listEntriesUseCases
} from "~/crud/contentEntry/useCases";

interface CreateContentEntryCrudParams {
Expand Down Expand Up @@ -1250,8 +1249,6 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm
);
},
/**
* TODO determine if this method is required at all.
*
* @internal
*/
async getEntry(model, params) {
Expand All @@ -1273,7 +1270,6 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm
});
},
async listLatestEntries<T = CmsEntryValues>(
this: HeadlessCms,
model: CmsModel,
params?: CmsEntryListParams
): Promise<[CmsEntry<T>[], CmsEntryMeta]> {
Expand All @@ -1285,7 +1281,6 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm
);
},
async listDeletedEntries<T = CmsEntryValues>(
this: HeadlessCms,
model: CmsModel,
params?: CmsEntryListParams
): Promise<[CmsEntry<T>[], CmsEntryMeta]> {
Expand Down
16 changes: 8 additions & 8 deletions packages/api-headless-cms/src/crud/contentModel.crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ export const createModelsCrud = (params: CreateModelsCrudParams): CmsModelContex
};

const managers = new Map<string, CmsModelManager>();
const updateManager = async (
const updateManager = async <T>(
context: CmsContext,
model: CmsModel
): Promise<CmsModelManager> => {
const manager = await contentModelManagerFactory(context, model);
): Promise<CmsModelManager<T>> => {
const manager = await contentModelManagerFactory<T>(context, model);
managers.set(model.modelId, manager);
return manager;
};
Expand Down Expand Up @@ -191,15 +191,15 @@ export const createModelsCrud = (params: CreateModelsCrudParams): CmsModelContex
});
};

const getEntryManager: CmsModelContext["getEntryManager"] = async (
target
): Promise<CmsModelManager> => {
const getEntryManager: CmsModelContext["getEntryManager"] = async <T>(
target: string | Pick<CmsModel, "modelId">
): Promise<CmsModelManager<T>> => {
const modelId = typeof target === "string" ? target : target.modelId;
if (managers.has(modelId)) {
return managers.get(modelId) as CmsModelManager;
return managers.get(modelId) as CmsModelManager<T>;
}
const model = await getModelFromCache(modelId);
return await updateManager(context, model);
return await updateManager<T>(context, model);
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ import { CmsModel, CmsContext, ModelManagerPlugin, CmsModelManager } from "~/typ

const defaultName = "content-model-manager-default";

export const contentModelManagerFactory = async (
export const contentModelManagerFactory = async <T>(
context: CmsContext,
model: CmsModel
): Promise<CmsModelManager> => {
): Promise<CmsModelManager<T>> => {
const pluginsByType = context.plugins
.byType<ModelManagerPlugin>("cms-content-model-manager")
.reverse();
for (const plugin of pluginsByType) {
const target = Array.isArray(plugin.modelId) ? plugin.modelId : [plugin.modelId];
if (target.includes(model.modelId) === true && plugin.name !== defaultName) {
return await plugin.create(context, model);
return await plugin.create<T>(context, model);
}
}
const plugin = pluginsByType.find(plugin => plugin.name === defaultName);
if (!plugin) {
throw new Error("There is no default plugin to create CmsModelManager");
}
return await plugin.create(context, model);
return await plugin.create<T>(context, model);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IHeadlessCmsLockRecord } from "~/lockingMechanism/types";

export interface IGetLockRecordUseCaseExecute {
(id: string): Promise<IHeadlessCmsLockRecord | null>;
}

export interface IGetLockRecordUseCase {
execute: IGetLockRecordUseCaseExecute;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IHeadlessCmsLockRecord, IHeadlessCmsLockRecordEntryType } from "~/lockingMechanism/types";

export interface ILockEntryUseCaseExecuteParams {
id: string;
type: IHeadlessCmsLockRecordEntryType;
}

export interface ILockEntryUseCaseExecute {
(params: ILockEntryUseCaseExecuteParams): Promise<IHeadlessCmsLockRecord>;
}

export interface ILockEntryUseCase {
execute: ILockEntryUseCaseExecute;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IHeadlessCmsLockRecordEntryType } from "~/lockingMechanism/types";

export interface IUnlockEntryUseCaseExecuteParams {
id: string;
type: IHeadlessCmsLockRecordEntryType;
}

export interface IUnlockEntryUseCaseExecute {
(params: IUnlockEntryUseCaseExecuteParams): Promise<void>;
}

export interface IUnlockEntryUseCase {
execute: IUnlockEntryUseCaseExecute;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IHeadlessCmsLockingMechanismIsLockedParams } from "~/lockingMechanism/types";

export type IIsEntryLockedUseCaseExecuteParams = IHeadlessCmsLockingMechanismIsLockedParams;

export interface IIsEntryLockedUseCaseExecute {
(params: IIsEntryLockedUseCaseExecuteParams): Promise<boolean>;
}

export interface IIsEntryLockedUseCase {
execute: IIsEntryLockedUseCaseExecute;
}
62 changes: 62 additions & 0 deletions packages/api-headless-cms/src/lockingMechanism/crud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { CmsContext } from "~/types";
import {
ICmsModelLockRecordManager,
IHeadlessCmsLockingMechanism,
IHeadlessCmsLockRecordValues
} from "./types";
import { createLockingModel, RECORD_LOCKING_MODEL_ID } from "./model";
import { IGetLockRecordUseCaseExecute } from "./abstractions/IGetLockRecordUseCase";
import { IIsEntryLockedUseCaseExecute } from "./abstractions/IsEntryLocked";
import { ILockEntryUseCaseExecute } from "~/lockingMechanism/abstractions/ILockEntryUseCase";
import { IUnlockEntryUseCaseExecute } from "~/lockingMechanism/abstractions/IUnlockEntryUseCase";
import { createUseCases } from "./useCases";

interface Params {
context: CmsContext;
}

export const createLockingMechanismCrud = ({ context }: Params): IHeadlessCmsLockingMechanism => {
context.plugins.register(createLockingModel());

const getManager = async (): Promise<ICmsModelLockRecordManager> => {
return await context.cms.getEntryManager<IHeadlessCmsLockRecordValues>(
RECORD_LOCKING_MODEL_ID
);
};

const { unlockEntryUseCase, lockEntryUseCase, getLockRecordUseCase, isEntryLockedUseCase } =
createUseCases({
getManager
});

const getLockRecord: IGetLockRecordUseCaseExecute = async (id: string) => {
return context.benchmark.measure("headlessCms.crud.locking.getLockRecord", async () => {
return getLockRecordUseCase.execute(id);
});
};

const isEntryLocked: IIsEntryLockedUseCaseExecute = async params => {
return context.benchmark.measure("headlessCms.crud.locking.isEntryLocked", async () => {
return isEntryLockedUseCase.execute(params);
});
};

const lockEntry: ILockEntryUseCaseExecute = async params => {
return context.benchmark.measure("headlessCms.crud.locking.lockEntry", async () => {
return lockEntryUseCase.execute(params);
});
};

const unlockEntry: IUnlockEntryUseCaseExecute = async params => {
return context.benchmark.measure("headlessCms.crud.locking.lockEntry", async () => {
return unlockEntryUseCase.execute(params);
});
};

return {
isEntryLocked,
getLockRecord,
lockEntry,
unlockEntry
};
};
61 changes: 61 additions & 0 deletions packages/api-headless-cms/src/lockingMechanism/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createCmsModel, createPrivateModel } from "~/plugins";

export const RECORD_LOCKING_MODEL_ID = "wby_recordLocking";

export const createLockingModel = () => {
return createCmsModel(
createPrivateModel({
modelId: RECORD_LOCKING_MODEL_ID,
name: "Record Lock Tracking",
fields: [
{
id: "targetId",
type: "text",
fieldId: "targetId",
storageId: "text@targetId",
label: "Target ID",
validation: [
{
name: "required",
message: "Target ID is required."
}
]
},
/**
* Since we need a generic way to track records, we will use type to determine if it's a cms record or a page or a form, etc...
* Update IHeadlessCmsLockRecordValues in types.ts file with additional fields as required.
*
* @see IHeadlessCmsLockRecordValues
*/
{
id: "type",
type: "text",
fieldId: "type",
storageId: "text@type",
label: "Record Type",
validation: [
{
name: "required",
message: "Record type is required."
},
/**
* Update pattern with additional types as required.
* Also update IHeadlessCmsLockRecordEntryType in types.ts file with additional types as required.
*/
{
name: "pattern",
message: "Record type is required.",
settings: {
pattern: {
name: "custom",
regex: "^pb:page|cms:([a-zA-Z0-9_-]+)$",
flags: ""
}
}
}
]
}
]
})
);
};
43 changes: 43 additions & 0 deletions packages/api-headless-cms/src/lockingMechanism/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { CmsIdentity, CmsModelManager } from "~/types";

export type ICmsModelLockRecordManager = CmsModelManager<IHeadlessCmsLockRecordValues>;

export interface IHeadlessCmsLockRecordValues {
targetId: string;
type: IHeadlessCmsLockRecordEntryType;
}

export interface IHeadlessCmsLockRecord {
id: string;
targetId: string;
type: IHeadlessCmsLockRecordEntryType;
lockedBy: CmsIdentity;
lockedOn: Date;
}

/**
* Do not use any special chars other than #, as we use this to create lock record IDs.
*/
export type IHeadlessCmsLockRecordEntryType = "pb#page" | `cms#${string}`;

export interface IHeadlessCmsLockingMechanismIsLockedParams {
id: string;
type: IHeadlessCmsLockRecordEntryType;
}

export interface IHeadlessCmsLockingMechanismLockEntryParams {
id: string;
type: IHeadlessCmsLockRecordEntryType;
}

export interface IHeadlessCmsLockingMechanismUnlockEntryParams {
id: string;
type: IHeadlessCmsLockRecordEntryType;
}

export interface IHeadlessCmsLockingMechanism {
getLockRecord(id: string): Promise<IHeadlessCmsLockRecord | null>;
isEntryLocked(params: IHeadlessCmsLockingMechanismIsLockedParams): Promise<boolean>;
lockEntry(params: IHeadlessCmsLockingMechanismLockEntryParams): Promise<IHeadlessCmsLockRecord>;
unlockEntry(params: IHeadlessCmsLockingMechanismUnlockEntryParams): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { IGetLockRecordUseCase } from "~/lockingMechanism/abstractions/IGetLockRecordUseCase";
import { ICmsModelLockRecordManager, IHeadlessCmsLockRecord } from "~/lockingMechanism/types";
import { NotFoundError } from "@webiny/handler-graphql";
import { convertEntryToLockRecord } from "~/lockingMechanism/utils/convertEntryToLockRecord";

export interface IGetLockRecordUseCaseParams {
getManager(): Promise<ICmsModelLockRecordManager>;
}

export class GetLockRecordUseCase implements IGetLockRecordUseCase {
private readonly getManager: IGetLockRecordUseCaseParams["getManager"];

public constructor(params: IGetLockRecordUseCaseParams) {
this.getManager = params.getManager;
}

public async execute(id: string): Promise<IHeadlessCmsLockRecord | null> {
try {
const manager = await this.getManager();
const result = await manager.get(id);
return convertEntryToLockRecord(result);
} catch (ex) {
if (ex instanceof NotFoundError) {
return null;
}
throw ex;
}
}
}
Loading

0 comments on commit 4571658

Please sign in to comment.