Add registering of client-side relationship properties (#5936)

This commit is contained in:
Tom Moor
2023-10-05 19:50:59 -04:00
committed by GitHub
parent e70d4e60fd
commit a2f037531a
7 changed files with 94 additions and 12 deletions

View File

@@ -34,6 +34,12 @@ class Comment extends Model {
@observable
parentCommentId: string;
/**
* The comment that this comment is a reply to.
*/
@Relation(() => Comment, { onDelete: "cascade" })
parentComment?: Comment;
/**
* The document to which this comment belongs.
*/

View File

@@ -47,7 +47,7 @@ class Notification extends Model {
/**
* The document that the notification is associated with.
*/
@Relation(() => Document)
@Relation(() => Document, { onDelete: "cascade" })
document?: Document;
/**
@@ -58,7 +58,7 @@ class Notification extends Model {
/**
* The comment that the notification is associated with.
*/
@Relation(() => Comment)
@Relation(() => Comment, { onDelete: "cascade" })
comment?: Comment;
/**

View File

@@ -95,12 +95,12 @@ export default abstract class Model {
*/
toAPI = (): Record<string, any> => {
const fields = getFieldsForModel(this);
return pick(this, fields) || [];
return pick(this, fields);
};
/**
* Returns a plain object representation of all the properties on the model
* overrides the inbuilt toJSON method to avoid attempting to serialize store
* overrides the native toJSON method to avoid attempting to serialize store
*
* @returns {Record<string, any>}
*/

View File

@@ -1,9 +1,9 @@
import type Model from "../base/Model";
const fields = new Map();
const fields = new Map<string, string[]>();
export const getFieldsForModel = (target: Model) =>
fields.get(target.constructor.name);
fields.get(target.constructor.name) ?? [];
/**
* A decorator that records this key as a serializable field on the model.
@@ -14,7 +14,10 @@ export const getFieldsForModel = (target: Model) =>
*/
const Field = <T>(target: any, propertyKey: keyof T) => {
const className = target.constructor.name;
fields.set(className, [...(fields.get(className) || []), propertyKey]);
fields.set(className, [
...(fields.get(className) || []),
propertyKey as string,
]);
};
export default Field;

View File

@@ -4,6 +4,47 @@ import type Model from "../base/Model";
type RelationOptions = {
/** Whether this relation is required */
required?: boolean;
/** Behavior of relationship on deletion */
onDelete: "cascade" | "null" | "ignore";
};
type RelationProperties = {
/** The name of the property on the model that stores the ID of the relation */
idKey: string;
/** A function that returns the class of the relation */
relationClassResolver: () => typeof Model;
/** Options for the relation */
options: RelationOptions;
};
type InverseRelationProperties = RelationProperties & {
/** The name of the model class that owns this relation */
modelName: string;
};
const relations = new Map<string, Map<string, RelationProperties>>(new Map());
/**
* Returns the inverse relation properties for the given model class.
*
* @param targetClass The model class to get inverse relations for.
* @returns A map of inverse relation properties keyed by the property name.
*/
export const getInverseRelationsForModelClass = (targetClass: typeof Model) => {
const inverseRelations = new Map<string, InverseRelationProperties>();
relations.forEach((relation, modelName) => {
relation.forEach((properties, propertyName) => {
if (properties.relationClassResolver().name === targetClass.name) {
inverseRelations.set(propertyName, {
...properties,
modelName,
});
}
});
});
return inverseRelations;
};
/**
@@ -20,8 +61,19 @@ export default function Relation<T extends typeof Model>(
) {
return function (target: any, propertyKey: string) {
const idKey = `${String(propertyKey)}Id`;
const relationClass = classResolver();
const relationClassName = relationClass.name;
// If the relation has options provided then register them in a map for later lookup. We can use
// this to determine how to update relations when a model is deleted.
if (options) {
const configForClass =
relations.get(target.constructor.name) || new Map();
configForClass.set(propertyKey, {
options,
relationClassResolver: classResolver,
idKey,
});
relations.set(target.constructor.name, configForClass);
}
Object.defineProperty(target, propertyKey, {
get() {
@@ -31,6 +83,7 @@ export default function Relation<T extends typeof Model>(
return undefined;
}
const relationClassName = classResolver().name;
const store =
this.store.rootStore[`${relationClassName.toLowerCase()}s`];
invariant(store, `Store for ${relationClassName} not found`);
@@ -41,6 +94,7 @@ export default function Relation<T extends typeof Model>(
this[idKey] = newValue ? newValue.id : undefined;
if (newValue) {
const relationClassName = classResolver().name;
const store =
this.store.rootStore[`${relationClassName.toLowerCase()}s`];
invariant(store, `Store for ${relationClassName} not found`);