Skip to content
This repository was archived by the owner on Apr 17, 2023. It is now read-only.

feat(datasync): add conflicts by version#1833

Merged
wtrocki merged 13 commits into
masterfrom
datasync-conflicts
Aug 12, 2020
Merged

feat(datasync): add conflicts by version#1833
wtrocki merged 13 commits into
masterfrom
datasync-conflicts

Conversation

@ssd71

@ssd71 ssd71 commented Aug 7, 2020

Copy link
Copy Markdown
Collaborator

Rather hopeful about this one 🤞

const { typeDefs, resolvers, contextCreator } = createDataSync(modelDefs, db, { Comment: { enabled: true, conflictResolution: serverSideConflictResolution }});

TODOS

  • Agreement on if this is how it should be
  • Delete Conflicts?
  • Tests
  • Docs

});

if (this.config.useVersion) {
if (this.config?.modelUsesVersion[model.graphqlType.name] || false) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to variable

const { _id, ...delta } = updateDocument;
const version = delta[DataSyncFieldNames.version];
// eslint-disable-next-line @typescript-eslint/tslint/config
delete delta[DataSyncFieldNames.version];delete delta[DataSyncFieldNames.lastUpdatedAt];delete delta[DataSyncFieldNames.deleted];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why deleting those fields... and why in single line :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting so as to ensure only the fields changed by the user are kept in the diff

}

// returns final resolved update document
public async resolveConflictFromDelta(updateDocument: any): Promise<any> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving conflict algorithm for dummies.

1. Accept client side version form client and validate it.
2. Look for the delta table entry with id and version.
3. This entry will be our `base` for conflict resolution.
4. Fetch server side entry 
5. Calculate diff for server side entry
5. Perform conflict check (check if fields that were changed are mergeable based on the diffs)
6. if mergable perform merge
7. If not mergable (the same fields changed - perform predefined conflict resolution.
8. Call out resolution algorithm (user provided function or default ones we provide)
We can use `MostRecentWins strategy` - that is why do not remove last updated :)
9.  try to save result to database - conditial check if version did not changed since then
7. Return data to user.

Another condition - if items reached their time (TTL ;) we have no way to kick offix compatible error to user.

All of this logic is already implemented in:
https://github.com/aerogear/offix/tree/master/packages/offix-conflicts-client

See also offix conflict server. Obviously you are free to invent your own algorithm but based on what I see I will probably prefer us to copy paste the code that was written and used in production rather than try to invent new way :)

dataProviderCreator: createDataSyncConflictProviderCreator(db, dataSyncConfig),
plugins:[
...graphbackConfig.plugins || [],
new DataSyncPlugin({ modelUsesVersion: Object.keys(dataSyncConfig).reduce((versionMap: any, modelName: string) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate variable?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually why we do not do this inside plugin. It feels wrong to do it here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 it should work the same if a user wants to use buildGraphbackAPI directly instead of createDataSync

export class MongoDeltaHelper {
protected collection: Collection;
public constructor(model: ModelDefinition, db: Db) {
this.collection = db.collection(getDeltaTableName(getTableName(model.graphqlType)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract variable

public async update(updateDocument: any, context: GraphbackContext): Promise<Type> {
const { _id } = updateDocument;

const MAX_RETRIES = 3;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not the best place to setup this value.



if (updateDocument[DataSyncFieldNames.version] !== serverStateData[DataSyncFieldNames.version]) {
const conflict: ConflictStateMap = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't mean conflict.
It means only that versions are different.
We need diffs to know if conflict occured.

Server data might have different fields than client as well.

Comment thread packages/graphback-datasync/src/DataSyncPlugin.ts Outdated
Comment thread packages/graphback-datasync/src/helpers/createDataSync.ts Outdated
@ssd71 ssd71 force-pushed the datasync-conflicts branch 2 times, most recently from 9c2724a to 601fe9f Compare August 10, 2020 07:02
@ssd71 ssd71 requested review from craicoverflow and wtrocki August 10, 2020 07:09
});

if (this.config.useVersion) {
const modelUsesVersion = !!this.config.modelConfigMap[model.graphqlType.name]?.enabled;

@craicoverflow craicoverflow Aug 10, 2020

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a double-negation is required here.

Suggested change
const modelUsesVersion = !!this.config.modelConfigMap[model.graphqlType.name]?.enabled;
const modelUsesVersion = this.config.modelConfigMap[model.graphqlType.name]?.enabled;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm wouldn't that make the expression true when the rest of the statement is undefined?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, what I mean to say I don't think there is a need for any negation here.

@machi1990 machi1990 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done a quick review and I've added my comments inline. Let me know what you think.

Comment thread packages/graphback-datasync/src/deltaHelper.ts Outdated
Comment thread packages/graphback-datasync/src/deltaHelper.ts
Comment thread packages/graphback-datasync/src/helpers/createDataSync.ts Outdated
Comment thread packages/graphback-datasync/src/providers/DataSyncConflictProvider.ts Outdated
Comment thread packages/graphback-datasync/src/providers/DataSyncConflictProvider.ts Outdated
Comment thread packages/graphback-datasync/src/providers/DataSyncConflictProvider.ts Outdated
Comment thread packages/graphback-datasync/src/DataSyncPlugin.ts Outdated
Comment thread packages/graphback-datasync/src/deltaHelper.ts Outdated
/**
* M
*/
export class MongoDeltaHelper {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a helper class is used to assist in providing some functionality, which isn't the main goal of the application or class in which it is used.

I think the name MongoDeltaHelper is too generic/non-specific. It is in fact a very integral class providing a very specific purpose. I see a helper class as being a collection of methods which do not necessarily work together or have common purpose.

Comment thread packages/graphback-datasync/src/deltaHelper.ts Outdated
Comment thread packages/graphback-datasync/src/deltaHelper.ts Outdated
this.collectionName = getDeltaTableName(getTableName(model.graphqlType));
}

public async insertDiff(updatedDocument: any) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could bring better type support to this class with generics:

export class MongoDeltaHelper<T = any>
...
public async insertDiff(updatedDocument: T) {


type DataSyncGraphbackConfig = Partial<GraphbackConfig>

export function createDataSyncAPI(model: string | GraphQLSchema, db: Db, dataSyncConfigMap: { [modelName: string]: DataSyncModelConflictConfig} = {}, graphbackConfig: DataSyncGraphbackConfig = {}): GraphbackAPI {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 5 arguments is too complicated for the user API, what do you think about reducing it to max 2?

DataSyncGraphbackConfig can copy the required config props (crud, plugins) from GraphbackAPIConfig. This means very minimal repetition, but greatly simplifies user experience, which is paramount IMO.

export function createDataSyncAPI(model: string | GraphQLSchema, config: DataSyncGraphbackConfig): GraphbackAPI {

Thoughts? Do you see any complications that might arise from this? cc @machi1990

import { createDataSyncCRUDService } from '../services';
import { DataSyncPlugin } from '../DataSyncPlugin';

type DataSyncGraphbackConfig = Partial<GraphbackConfig>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraphbackConfig is not used in runtime API (it is used for the generate command. I think you are looking for GraphbackAPIConfig?

export function createDataSyncAPI(model: string | GraphQLSchema, db: Db, dataSyncConfigMap: { [modelName: string]: DataSyncModelConflictConfig} = {}, graphbackConfig: DataSyncGraphbackConfig = {}): GraphbackAPI {

return buildGraphbackAPI(model, {
serviceCreator: createDataSyncCRUDService({ pubSub: new PubSub() }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if user wants to use their own PubSub library?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serviceCreator should still be an (optional) option in the API config, as the user may want to customise/extend it.

export const MAX_RETRIES = 3;

/**
* Da

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requires API docs

return result;
}

public async update(updateDocument: any, context: GraphbackContext): Promise<Type> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public async update(updateDocument: any, context: GraphbackContext): Promise<Type> {
public async update(updateDocument: Partial<Type>, context: GraphbackContext): Promise<Type> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be done

Comment thread packages/graphback-datasync/src/providers/DataSyncConflictProvider.ts Outdated

const serverData = await this.db.collection(this.collectionName).findOne({ _id, [DataSyncFieldNames.deleted]: false});
const base = await this.deltaHelper.findBaseForConflicts(updateDocument);
const clientData = updateDocument;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use this separate variable which references updateDocument? I don't see updateDocument being modified after this (unless I missed it)

Comment thread packages/graphback-datasync/src/providers/DataSyncConflictProvider.ts Outdated
@ssd71 ssd71 force-pushed the datasync-conflicts branch 2 times, most recently from ebfdfbf to cecccf0 Compare August 12, 2020 13:17
},
resolveDelete(conflict: ConflictMetadata): any {

throw new ConflictError(conflict);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to know what operation thrown conflict (in case someone will start batching requests on client)

Separate issue probably?

Comment thread packages/graphback-datasync/src/util.ts Outdated
@wtrocki

wtrocki commented Aug 12, 2020

Copy link
Copy Markdown
Contributor

This looks very very good.
I would merge this in order to start playing with this in other project but we need more integration tests.

@ssd71 ssd71 force-pushed the datasync-conflicts branch from dec2a81 to 357f466 Compare August 12, 2020 15:48
@ssd71

ssd71 commented Aug 12, 2020

Copy link
Copy Markdown
Collaborator Author

Fixed clientWins strategy to force deletes as well as added some more integration tests for deletion

@ssd71 ssd71 requested a review from wtrocki August 12, 2020 15:53
}
}
}});
console.log(errors);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth to assert this to be empty. :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Datasync enduser api

4 participants