Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save craicoverflow/a165dbb42688e26d794f6f0a18bd2ebc to your computer and use it in GitHub Desktop.
Save craicoverflow/a165dbb42688e26d794f6f0a18bd2ebc to your computer and use it in GitHub Desktop.
import { GraphQLObjectType } from 'graphql';
import { NoDataError } from '@graphback/runtime';
import { getDatabaseArguments } from '@graphback/core';
import { ObjectId } from 'mongodb';
import { MongoDBDataProvider } from './MongoDBDataProvider';
enum GraphbackDirective {
UpdatedAt = 'updatedAt'
}
interface FieldDirectiveTransformer {
name: string
fieldName: string
fieldTransform: Function
}
/**
* Go through all fields in the model and build up a list of directives in each
* Each directive has a fieldTransform field that binds some logic to that field
* @param modelType Model
*/
function createDirectiveTransformers(modelType: GraphQLObjectType): FieldDirectiveTransformer[] {
const modelFields = Object.values(modelType.getFields())
const fieldDirectiveTransformers: FieldDirectiveTransformer[] = []
for (const field of modelFields) {
const fieldDirectives = field.astNode.directives
for (const directive of fieldDirectives) {
if (directive.name.value === 'updatedAt') {
fieldDirectiveTransformers.push({
name: GraphbackDirective.UpdatedAt,
fieldName: field.name,
fieldTransform: () => {
return (new Date())
}
})
}
}
}
return fieldDirectiveTransformers
}
/**
* Mongo provider that contains special handlers for offix conflict resolution format:
*
* https://offix.dev/docs/conflict-server#structure-of-the-conflict-error
*/
export class OffixMongoDBDataProvider<Type = any, GraphbackContext = any> extends MongoDBDataProvider<Type, GraphbackContext> {
protected fieldDirectiveTransformers: FieldDirectiveTransformer[]
public constructor(baseType: GraphQLObjectType, client: any) {
super(baseType, client);
this.fieldDirectiveTransformers = createDirectiveTransformers(baseType)
}
public async update(data: any): Promise<Type> {
const { idField } = getDatabaseArguments(this.tableMap, data);
if (!idField.value) {
throw new NoDataError(`Cannot update ${this.collectionName} - missing ID field`)
}
// TODO Can be improved by conditional updates
const queryResult = await this.db.collection(this.collectionName).find({ _id: new ObjectId(idField.value) }).toArray();
if (queryResult && queryResult[0]) {
queryResult[0][idField.name] = queryResult[0]._id;
if (data.version !== queryResult[0].version) {
const conflictError: any = new Error();
conflictError.conflictInfo = { serverState: queryResult[0], clientState: data };
throw conflictError
}
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
data.version = data.version + 1;
// get the updated at directive if it exists
const updatedAtDirective = this.fieldDirectiveTransformers.find((v => v.name === GraphbackDirective.UpdatedAt))
// apply updated at directive
// all logic to apply this is in the FieldDirectiveTransformer
if (updatedAtDirective) {
document[updatedAtDirective.fieldName] = updatedAtDirective.fieldTransform()
}
// TODO use findOneAndUpdate to check consistency afterwards
const result = await this.db.collection(this.collectionName).updateOne({ _id: new ObjectId(idField.value) }, { $set: data });
if (result.result?.ok) {
return this.mapFields(data);
}
}
throw new NoDataError(`Cannot update ${this.collectionName}`);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment