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:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### 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
|
||||
|
|
@ -262,7 +262,7 @@ The error message should explain the allowed alternatives:
|
|||
|
||||
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;
|
||||
- rejecting `sync -> non-sync` restricted reference-table FKs with
|
||||
`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, '
|
||||
'but ${tablesWithoutScopeId.length} table(s) are missing it or declare '
|
||||
'it with the wrong type: '
|
||||
'${tablesWithoutScopeId.map((t) => '"${t.tableName}"').join(', ')}\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?\n\n'
|
||||
'If the column is declared with scope=serverOnly, remove the scope; '
|
||||
'the column must exist on every database.',
|
||||
'${tablesWithoutScopeId.map((t) => '"${t.tableName}"').join(', ')}'
|
||||
'$_scopeIdFieldHelp',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -63,8 +57,34 @@ class CrdtSchemaRegistry {
|
|||
'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;
|
||||
|
||||
/// The list of tables to sync with CRDT.
|
||||
|
|
@ -210,3 +230,27 @@ bool _isForbiddenGlobalUniqueIndex(
|
|||
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:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
street: String
|
||||
inhabitantId: UuidValue?, unique
|
||||
inhabitant: Person?, relation(name=person_address, field=inhabitantId, onDelete=Restrict)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
citizens: List<Person>?, relation
|
||||
organizations: List<Organization>?, relation(name=city_org)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
town: Town?, relation(field=townId, onDelete=SetDefault)
|
||||
townId: UuidValue, default='550e8400-e29b-41d4-a716-446655440000'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
root: FkChainRoot?, relation(field=rootId, onDelete=Cascade)
|
||||
rootId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
restrictBlocker: FkChainRestrictBlocker?, relation(field=restrictBlockerId, onDelete=Cascade)
|
||||
restrictBlockerId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
restrictBlocker: FkChainRestrictBlocker?, relation(field=restrictBlockerId, onDelete=SetNull)
|
||||
restrictBlockerId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
cascadeMiddle: FkChainCascadeMiddle?, relation(field=cascadeMiddleId, onDelete=Restrict)
|
||||
cascadeMiddleId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
setNullMiddle: FkChainSetNullMiddle?, relation(field=setNullMiddleId, onDelete=Cascade)
|
||||
setNullMiddleId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
cascadeMiddle: FkChainCascadeMiddle?, relation(field=cascadeMiddleId, onDelete=SetNull)
|
||||
cascadeMiddleId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
setNullMiddle: FkChainSetNullMiddle?, relation(field=setNullMiddleId, onDelete=Restrict)
|
||||
setNullMiddleId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
setNullMiddle: FkChainSetNullMiddle?, relation(field=setNullMiddleId, onDelete=SetNull)
|
||||
setNullMiddleId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
people: List<Person>?, relation(name=org_person)
|
||||
city: City?, relation(name=city_org, optional, onDelete=Cascade)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
surname: String?
|
||||
address: Address?, relation(name=person_address)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
parent: Person?, relation(field=parentId, onDelete=SetNull)
|
||||
parentId: UuidValue
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
parent: Person?, relation(field=parentId, onDelete=Restrict)
|
||||
parentId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
name: String
|
||||
city: City?, relation(optional, onDelete=Cascade)
|
||||
mayor: Person?, relation(optional, onDelete=SetNull)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ database: all
|
|||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
aBool: bool
|
||||
aDateTime: DateTime
|
||||
aText: String
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue