Add delta table#1647
Conversation
| delete data.id; | ||
|
|
||
| const res = await this.db.collection(this.collectionName).insertOne({ | ||
| doc: data |
There was a problem hiding this comment.
We might need more information here. Like what operation was executed etc.
|
|
||
| public sync(lastSync: string, context: GraphbackContext, filter?: any): Promise<Type[]> { | ||
|
|
||
| return this.deltaProvider.sync(parseInt(lastSync, 10)).then((res: any[]) => { |
There was a problem hiding this comment.
So we went to the 1.5 - WE have separate delta table with duplicate of the data. (Not really delta table itself.)?
There was a problem hiding this comment.
It is open for discussion, really, I just wanted get some feedback, I haven't hacked around with it that much to make a decisive move yet
There was a problem hiding this comment.
yep. No worries. This works and it is very very well written. But we cannot get conflict information from it. Let's drop some short research to see how we can get that done
There was a problem hiding this comment.
So we went to the 1.5 - WE have separate delta table with duplicate of the data. (Not really delta table itself.)?
This should be more of a changelog + auditing table. This article might be of help https://ludwigstuyck.wordpress.com/2013/04/04/history-tracking/
| this.deltaProvider.insertDiff(result.value).catch((e: any) => { | ||
| console.error(`Error: Couldn't insert into delta table: ${e}`); | ||
| }) |
There was a problem hiding this comment.
This block in update, delete and create seem to be the only things that make this provider different to DatasyncMongoDBDataProvider.
Could this just extend that instead, to save repeating code in two providers and increasing maintenance costs.
Or would it be feasible to use the same provider, and only execute this block if this.deltaProvider has been created, which could be conditionally created based on the existence of the the deltaTable annotation key.
This got me thinking also - maybe we should be passing the ModelDefinition to the provider instead of the base GraphQL type, as a lot of the preparsed metadata information is not available from the base type and you would have to re-parse it.
There was a problem hiding this comment.
This got me thinking also - maybe we should be passing the ModelDefinition to the provider instead of the base GraphQL type, as a lot of the preparsed metadata information is not available from the base type and you would have to re-parse it.
+1, this rejoins this comment #1614 (comment)
There was a problem hiding this comment.
I've left one comment: https://github.com/aerogear/graphback/pull/1647/files#r449454898
Is there a related issue for this PR (I know it is a spike, but maybe some background).
|
Hi @craicoverflow , sorry for the late response |
To test out with datasync template: """
@model
@datasync(deltaTable: true)
"""
type Comment {
id: ID!
text: String
description: String
}and in import { createDataSyncCRUDService, DataSyncPlugin, createDataSyncMongoDbProvider, DeltaConflictEngine } from '@graphback/datasync';
...
const { typeDefs, resolvers, contextCreator } = buildGraphbackAPI(modelDefs, {
dataProviderCreator: createDataSyncMongoDbProvider(db, { Comment: DeltaConflictEngine }),
serviceCreator: createDataSyncCRUDService({ pubSub: new PubSub() }),
plugins: [
new DataSyncPlugin()
]
}); |
craicoverflow
left a comment
There was a problem hiding this comment.
Tried to verify but I don't know how to use createDataSyncMongoDbProvider with the new conflictStrategyMap param. Could you update the PR description with step-by-step guide how to verify this?
| checkForConflicts(serverState: any, clientState: any): ConflictStateMap | undefined | ||
|
|
||
| /** | ||
| * checkForConflicts |
There was a problem hiding this comment.
| * checkForConflicts | |
| * resolveConflicts |
| } | ||
|
|
||
| /** | ||
| * d |
| * d | ||
| */ | ||
| export abstract class CustomConflictEngine implements ConflictEngine { | ||
| protected conflictFieldName: string = undefined; |
There was a problem hiding this comment.
| protected conflictFieldName: string = undefined; | |
| protected conflictFieldName: string; |
This should be the same.
| protected conflictFieldName: string = undefined; | ||
|
|
||
| public checkForConflicts(serverState: any, clientState: any): ConflictStateMap { | ||
| if (serverState[this.conflictFieldName] !== undefined && clientState[this.conflictFieldName].toString() !== serverState[fieldNames.updatedAt].toString()) { |
There was a problem hiding this comment.
This statement is very long, wondering if we can make it smaller or else break it into a couple of lines
| if (serverState[this.conflictFieldName] !== undefined && clientState[this.conflictFieldName].toString() !== serverState[fieldNames.updatedAt].toString()) { | |
| if (serverState[this.conflictFieldName] && clientState[this.conflictFieldName].toString() !== serverState[fieldNames.updatedAt].toString()) { |
| export class DeltaConflictEngine extends TimestampConflictEngine { | ||
| public checkForConflicts(serverDelta: any, clientSets: any): ConflictStateMap { | ||
|
|
||
| if (super.checkForConflicts(serverDelta, clientSets) === undefined) { |
There was a problem hiding this comment.
If the returned result is ever null instead of undefined then this won't evaluate as expected as null !== undefined. Doing this will work for both possibilities:
| if (super.checkForConflicts(serverDelta, clientSets) === undefined) { | |
| if (!super.checkForConflicts(serverDelta, clientSets)) { |
|
|
||
| export function getDiffEntry(data: any): DiffEntry { | ||
| const sets = Object.assign({}, data); | ||
| const objectId = sets.id instanceof ObjectId ? sets.id : new ObjectId(sets.id); |
There was a problem hiding this comment.
I've seen this used somewhere else too. Would be nice in a helper function.
| if (isDataSyncModel(model)) { | ||
| return new DataSyncMongoDBDataProvider(model.graphqlType, db) | ||
| const annotationData = isDataSyncModel(model); | ||
| if (annotationData) { |
There was a problem hiding this comment.
You could improve readibility if you try to avoid nested ifs (where possible):
if (!annotationData) {
return new MongoDBDataProvider(model.graphqlType, db)
}
let conflictStrategy: new() => ConflictEngine;
if (conflictStrategyMap !== undefined) {
conflictStrategy = conflictStrategyMap[model.graphqlType.name];
}
if (annotationData.deltaTable === true) {
return new DeltaDBDataProvider(model.graphqlType, db, conflictStrategy);
}
return new DataSyncMongoDBDataProvider(model.graphqlType, db, conflictStrategy);| if (conflictStrategyMap !== undefined) { | ||
| conflictStrategy = conflictStrategyMap[model.graphqlType.name]; | ||
| } | ||
| if (annotationData.deltaTable === true) { |
There was a problem hiding this comment.
annotationData can be boolean, so this would throw an error.
| import { DataSyncCRUDService } from "./services"; | ||
|
|
||
| export function isDataSyncModel(model: ModelDefinition): boolean { | ||
| export function isDataSyncModel(model: ModelDefinition): any { |
There was a problem hiding this comment.
If this can now return the annotation data the function name should change too.
| public async update(data: Type, context: GraphbackContext): Promise<Type> { | ||
| const { idField } = getDatabaseArguments(this.tableMap, data); | ||
| const updateSets = Object.assign({}, data as any); | ||
| delete updateSets.id; |
There was a problem hiding this comment.
getDatabaseArguments will return an ID field different to id if the @id annotation is different. So should this be deleting that key instead?
|
@craicoverflow Sorry for lack of proper instructions, I have updated PR description a bit, please test it out at your leisure |
|
No problem, thanks for updating :) Should this still be in draft btw? Or are you seeking more implementation feedback first. |
|
Looking for implementation feedback actually, not very confident about it, would definitely appreciate it 😄 |
| } | ||
| const queryResult = await this.getServerState(clientData); | ||
| if (queryResult) { | ||
| const conflict = this.conflictEngine.checkForConflicts(queryResult, clientData) |
There was a problem hiding this comment.
I think while this made sense for the Postgress when we were building it for Mongo this approach makes less sense. There could be some operation happening in the middle of those two.
Approach here could be that we can research mongodb optimistic locking
We should check if we can have conditional errors in mongo - error that is returned if query do not accept some condition.
While this approach is ok.. it is definitely not production ready as consistency is not guaranteed here.
| timestamp: Date | ||
| } | ||
|
|
||
| export function getDiffEntry(data: any): DiffEntry { |
There was a problem hiding this comment.
I would love to get more explaination what you creating here.
I see couple fields removed that I would consider essential.
|
@ssd71 Results of the investigations are usually presented in the form of the docs. It is nice to have working code for it, but if we getting just code and no info about the approach or strategy you are taking here it is not possible to really move forward. We want to investigate proper patterns and document how we see this working. Step by step kinda approach. Maybe some quick diagram etc. |
|
Results of investigation and spike documented in #1738 |
This is a spike towards investigating Delta Tables for datasync, can be tested out as follows:
This is enabled in schema
Use the following schema with datasync template
It should create a comment_delta collection in the database
Conflicts are enabled in code
Import TimestampConflictEngine from datasync package:
Provide a map of model name to conflict strategy(pardon the horrendous inconsistency in the names, I would love some feedback about what to call them) in createDataSyncMongoDbProvider as follows:
This tells it to use Timestamp based conflicts for the Comment model, there is also another strategy intended for use with delta tables called DeltaConflictEngine, it checks if the fields to be updated have changed since the client passed
updatedAttimestamp, and raises a conflict if they have, I would love some feedback about this as well.Note, if using delta table, sync query only returns fields that have changed, not sure this is optimal so feedback needed