feat: Require scopeId cascade FK relation to crdt_scopes for synced tables (#79)
Co-authored-by: Marcelo Soares <marcelo.me.soares@gmail.com>
This commit is contained in:
parent
38fa00ddfc
commit
5642d9b94d
27 changed files with 393 additions and 35 deletions
|
|
@ -24,7 +24,7 @@ The ownership link is:
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?, relation(optional, parent=crdt_scopes, onDelete=Cascade)
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
```
|
```
|
||||||
|
|
||||||
`scopeId == null` remains valid persisted state, but it means the row is
|
`scopeId == null` remains valid persisted state, but it means the row is
|
||||||
|
|
@ -262,7 +262,7 @@ The error message should explain the allowed alternatives:
|
||||||
|
|
||||||
The implementation issue should add schema/initialize tests for:
|
The implementation issue should add schema/initialize tests for:
|
||||||
|
|
||||||
- accepting `scopeId: int?, relation(optional, parent=crdt_scopes,
|
- accepting `scopeId: int?, relation(parent=crdt_scopes,
|
||||||
onDelete=Cascade)` on every synced table;
|
onDelete=Cascade)` on every synced table;
|
||||||
- rejecting `sync -> non-sync` restricted reference-table FKs with
|
- rejecting `sync -> non-sync` restricted reference-table FKs with
|
||||||
`onDelete=Restrict` while the reference-data sync phase is absent;
|
`onDelete=Restrict` while the reference-data sync phase is absent;
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,8 @@ class CrdtSchemaRegistry {
|
||||||
'CRDT can only synchronize tables with a nullable int scopeId column, '
|
'CRDT can only synchronize tables with a nullable int scopeId column, '
|
||||||
'but ${tablesWithoutScopeId.length} table(s) are missing it or declare '
|
'but ${tablesWithoutScopeId.length} table(s) are missing it or declare '
|
||||||
'it with the wrong type: '
|
'it with the wrong type: '
|
||||||
'${tablesWithoutScopeId.map((t) => '"${t.tableName}"').join(', ')}\n\n'
|
'${tablesWithoutScopeId.map((t) => '"${t.tableName}"').join(', ')}'
|
||||||
'Add this field to every synced model:\n'
|
'$_scopeIdFieldHelp',
|
||||||
'fields:\n'
|
|
||||||
' id: UuidValue?, defaultPersist=random_v7\n'
|
|
||||||
' ### Owner scope of this row. Maintained by the CRDT sync layer.\n'
|
|
||||||
' scopeId: int?\n\n'
|
|
||||||
'If the column is declared with scope=serverOnly, remove the scope; '
|
|
||||||
'the column must exist on every database.',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,8 +57,34 @@ class CrdtSchemaRegistry {
|
||||||
'whose target tables are also synchronized.',
|
'whose target tables are also synchronized.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final tablesMissingScopeIdRelation = _missingCrdtScopeRelations(
|
||||||
|
syncTables,
|
||||||
|
tableDefinitionsByName,
|
||||||
|
);
|
||||||
|
if (tablesMissingScopeIdRelation.isNotEmpty) {
|
||||||
|
throw StateError(
|
||||||
|
'CRDT synced tables must declare scopeId as a cascade relation to '
|
||||||
|
'crdt_scopes, but ${tablesMissingScopeIdRelation.length} table(s) are '
|
||||||
|
'missing this relation: '
|
||||||
|
'${tablesMissingScopeIdRelation.map((t) => '"$t"').join(', ')}'
|
||||||
|
'$_scopeIdFieldHelp',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Help text appended to schema validation errors that describes the required
|
||||||
|
/// `scopeId` ownership field on every synced model.
|
||||||
|
static const _scopeIdFieldHelp =
|
||||||
|
'\n\n'
|
||||||
|
'Add this field to every synced model:\n'
|
||||||
|
'fields:\n'
|
||||||
|
' id: UuidValue?, defaultPersist=random_v7\n'
|
||||||
|
' ### Owner scope of this row. Maintained by the CRDT sync layer.\n'
|
||||||
|
' scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)\n\n'
|
||||||
|
'If the column is declared with scope=serverOnly, remove the scope; '
|
||||||
|
'the column must exist on every database.';
|
||||||
|
|
||||||
final DatabaseSession _session;
|
final DatabaseSession _session;
|
||||||
|
|
||||||
/// The list of tables to sync with CRDT.
|
/// The list of tables to sync with CRDT.
|
||||||
|
|
@ -210,3 +230,27 @@ bool _isForbiddenGlobalUniqueIndex(
|
||||||
syncTableNames,
|
syncTableNames,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> _missingCrdtScopeRelations(
|
||||||
|
List<Table> syncTables,
|
||||||
|
Map<String, TableDefinition> tableDefinitionsByName,
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
for (final table in syncTables)
|
||||||
|
if (tableDefinitionsByName.containsKey(table.tableName) &&
|
||||||
|
!_hasCrdtScopeRelation(tableDefinitionsByName[table.tableName]!))
|
||||||
|
table.tableName,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasCrdtScopeRelation(TableDefinition tableDefinition) {
|
||||||
|
return tableDefinition.foreignKeys.any(
|
||||||
|
(fk) =>
|
||||||
|
fk.columns.length == 1 &&
|
||||||
|
fk.columns.single == 'scopeId' &&
|
||||||
|
fk.referenceTable == 'crdt_scopes' &&
|
||||||
|
fk.referenceColumns.length == 1 &&
|
||||||
|
fk.referenceColumns.single == 'id' &&
|
||||||
|
fk.onDelete == ForeignKeyAction.cascade,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
street: String
|
street: String
|
||||||
inhabitantId: UuidValue?, unique
|
inhabitantId: UuidValue?, unique
|
||||||
inhabitant: Person?, relation(name=person_address, field=inhabitantId, onDelete=Restrict)
|
inhabitant: Person?, relation(name=person_address, field=inhabitantId, onDelete=Restrict)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
citizens: List<Person>?, relation
|
citizens: List<Person>?, relation
|
||||||
organizations: List<Organization>?, relation(name=city_org)
|
organizations: List<Organization>?, relation(name=city_org)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
town: Town?, relation(field=townId, onDelete=SetDefault)
|
town: Town?, relation(field=townId, onDelete=SetDefault)
|
||||||
townId: UuidValue, default='550e8400-e29b-41d4-a716-446655440000'
|
townId: UuidValue, default='550e8400-e29b-41d4-a716-446655440000'
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
root: FkChainRoot?, relation(field=rootId, onDelete=Cascade)
|
root: FkChainRoot?, relation(field=rootId, onDelete=Cascade)
|
||||||
rootId: UuidValue?
|
rootId: UuidValue?
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
restrictBlocker: FkChainRestrictBlocker?, relation(field=restrictBlockerId, onDelete=Cascade)
|
restrictBlocker: FkChainRestrictBlocker?, relation(field=restrictBlockerId, onDelete=Cascade)
|
||||||
restrictBlockerId: UuidValue?
|
restrictBlockerId: UuidValue?
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
restrictBlocker: FkChainRestrictBlocker?, relation(field=restrictBlockerId, onDelete=SetNull)
|
restrictBlocker: FkChainRestrictBlocker?, relation(field=restrictBlockerId, onDelete=SetNull)
|
||||||
restrictBlockerId: UuidValue?
|
restrictBlockerId: UuidValue?
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
cascadeMiddle: FkChainCascadeMiddle?, relation(field=cascadeMiddleId, onDelete=Restrict)
|
cascadeMiddle: FkChainCascadeMiddle?, relation(field=cascadeMiddleId, onDelete=Restrict)
|
||||||
cascadeMiddleId: UuidValue?
|
cascadeMiddleId: UuidValue?
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,5 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
setNullMiddle: FkChainSetNullMiddle?, relation(field=setNullMiddleId, onDelete=Cascade)
|
setNullMiddle: FkChainSetNullMiddle?, relation(field=setNullMiddleId, onDelete=Cascade)
|
||||||
setNullMiddleId: UuidValue?
|
setNullMiddleId: UuidValue?
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
cascadeMiddle: FkChainCascadeMiddle?, relation(field=cascadeMiddleId, onDelete=SetNull)
|
cascadeMiddle: FkChainCascadeMiddle?, relation(field=cascadeMiddleId, onDelete=SetNull)
|
||||||
cascadeMiddleId: UuidValue?
|
cascadeMiddleId: UuidValue?
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
setNullMiddle: FkChainSetNullMiddle?, relation(field=setNullMiddleId, onDelete=Restrict)
|
setNullMiddle: FkChainSetNullMiddle?, relation(field=setNullMiddleId, onDelete=Restrict)
|
||||||
setNullMiddleId: UuidValue?
|
setNullMiddleId: UuidValue?
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
setNullMiddle: FkChainSetNullMiddle?, relation(field=setNullMiddleId, onDelete=SetNull)
|
setNullMiddle: FkChainSetNullMiddle?, relation(field=setNullMiddleId, onDelete=SetNull)
|
||||||
setNullMiddleId: UuidValue?
|
setNullMiddleId: UuidValue?
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
people: List<Person>?, relation(name=org_person)
|
people: List<Person>?, relation(name=org_person)
|
||||||
city: City?, relation(name=city_org, optional, onDelete=Cascade)
|
city: City?, relation(name=city_org, optional, onDelete=Cascade)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
surname: String?
|
surname: String?
|
||||||
address: Address?, relation(name=person_address)
|
address: Address?, relation(name=person_address)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
parent: Person?, relation(field=parentId, onDelete=SetNull)
|
parent: Person?, relation(field=parentId, onDelete=SetNull)
|
||||||
parentId: UuidValue
|
parentId: UuidValue
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
parent: Person?, relation(field=parentId, onDelete=Restrict)
|
parent: Person?, relation(field=parentId, onDelete=Restrict)
|
||||||
parentId: UuidValue?
|
parentId: UuidValue?
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
name: String
|
name: String
|
||||||
city: City?, relation(optional, onDelete=Cascade)
|
city: City?, relation(optional, onDelete=Cascade)
|
||||||
mayor: Person?, relation(optional, onDelete=SetNull)
|
mayor: Person?, relation(optional, onDelete=SetNull)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ database: all
|
||||||
fields:
|
fields:
|
||||||
id: UuidValue?, defaultPersist=random_v7
|
id: UuidValue?, defaultPersist=random_v7
|
||||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||||
scopeId: int?
|
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||||
aBool: bool
|
aBool: bool
|
||||||
aDateTime: DateTime
|
aDateTime: DateTime
|
||||||
aText: String
|
aText: String
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue