feat: Implement the scope management interface (#96)
This commit is contained in:
parent
467df68a69
commit
d89118a43a
3 changed files with 755 additions and 227 deletions
|
|
@ -10,6 +10,8 @@ void main() {
|
|||
withServerpod(
|
||||
'Given a database-backed CRDT scope sync session,',
|
||||
(sessionBuilder, _) {
|
||||
sessionBuilder.build().serverpod.initializeCrdtSync(syncTables: []);
|
||||
|
||||
late Session session;
|
||||
setUp(() async {
|
||||
session = sessionBuilder.build();
|
||||
|
|
@ -20,13 +22,7 @@ void main() {
|
|||
'then only the authoritative session announces grants.',
|
||||
() async {
|
||||
final userUuid = _uuid(30);
|
||||
final sharedScopeUuid = _uuid(20);
|
||||
|
||||
await _upsertScopeMembership(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
);
|
||||
final sharedScopeUuid = await session.crdt.scopes.createFor(userUuid);
|
||||
|
||||
final authoritative = CrdtScopeSyncSession(
|
||||
session,
|
||||
|
|
@ -73,14 +69,8 @@ void main() {
|
|||
'then it keeps cycling only its locally resolved scopes.',
|
||||
() async {
|
||||
final userUuid = _uuid(31);
|
||||
final localSharedScopeUuid = _uuid(21);
|
||||
final peerOnlyScopeUuid = _uuid(11);
|
||||
|
||||
await _upsertScopeMembership(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: localSharedScopeUuid,
|
||||
);
|
||||
final localSharedScopeUuid = await session.crdt.scopes.createFor(userUuid);
|
||||
|
||||
final authoritative = CrdtScopeSyncSession(
|
||||
session,
|
||||
|
|
@ -104,7 +94,6 @@ void main() {
|
|||
'then adoption keeps the announced scope set until the next reconcile.',
|
||||
() async {
|
||||
final userUuid = _uuid(32);
|
||||
final laterSharedScopeUuid = _uuid(22);
|
||||
|
||||
final authoritative = CrdtScopeSyncSession(
|
||||
session,
|
||||
|
|
@ -116,11 +105,7 @@ void main() {
|
|||
await authoritative.reconcile();
|
||||
authoritative.markAnnounced();
|
||||
|
||||
await _upsertScopeMembership(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: laterSharedScopeUuid,
|
||||
);
|
||||
final laterSharedScopeUuid = await session.crdt.scopes.createFor(userUuid);
|
||||
|
||||
await authoritative.adoptPeerGrants(const []);
|
||||
|
||||
|
|
@ -143,10 +128,17 @@ void main() {
|
|||
final announcedScopeUuid = _uuid(23);
|
||||
final staleScopeUuid = _uuid(13);
|
||||
|
||||
await _upsertScopeMembership(
|
||||
final scope = await CrdtScopeManager(session).getOrCreate(staleScopeUuid);
|
||||
|
||||
await CrdtScopeMember.db.upsertRow(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: staleScopeUuid,
|
||||
CrdtScopeMember(
|
||||
scopeId: scope.id!,
|
||||
userUuid: userUuid,
|
||||
role: CrdtScopeRole.readWrite,
|
||||
),
|
||||
conflictColumns: (t) => [t.scopeId, t.userUuid],
|
||||
updateColumns: (t) => [t.role],
|
||||
);
|
||||
|
||||
final follower = CrdtScopeSyncSession(
|
||||
|
|
@ -317,7 +309,6 @@ void main() {
|
|||
'then the tracked checkpoint moves to the change HLC.',
|
||||
() async {
|
||||
final userUuid = _uuid(37);
|
||||
final scopeUuid = _uuid(27);
|
||||
final localNodeUuid = _uuid(47);
|
||||
final remoteNodeUuid = _uuid(107);
|
||||
|
||||
|
|
@ -328,11 +319,7 @@ void main() {
|
|||
peerNodeId: _uuid(98),
|
||||
);
|
||||
|
||||
await _upsertScopeMembership(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: scopeUuid,
|
||||
);
|
||||
final scopeUuid = await session.crdt.scopes.createFor(userUuid);
|
||||
|
||||
await authoritative.reconcile();
|
||||
authoritative.recordPeerHandshake(
|
||||
|
|
@ -376,19 +363,3 @@ CrdtSyncSinceHlc _since(UuidValue scopeUuid, Hlc checkpoint) =>
|
|||
|
||||
Hlc _hlc(UuidValue nodeUuid, {required int minute}) =>
|
||||
Hlc(DateTime.utc(2026, 7, 2, 12, minute), 0, nodeUuid);
|
||||
|
||||
Future<void> _upsertScopeMembership(
|
||||
DatabaseSession session, {
|
||||
required UuidValue userUuid,
|
||||
required UuidValue scopeUuid,
|
||||
CrdtScopeRole role = CrdtScopeRole.readWrite,
|
||||
}) async {
|
||||
final scope = await CrdtScopeManager(session).getOrCreate(scopeUuid);
|
||||
|
||||
await CrdtScopeMember.db.upsertRow(
|
||||
session,
|
||||
CrdtScopeMember(scopeId: scope.id!, userUuid: userUuid, role: role),
|
||||
conflictColumns: (t) => [t.scopeId, t.userUuid],
|
||||
updateColumns: (t) => [t.role],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,7 +170,6 @@ void main() {
|
|||
|
||||
final serverSubscription = rawServerSession.crdt
|
||||
.sync(
|
||||
serverSession,
|
||||
userId: testCrdtUserId,
|
||||
inbound: clientToServer.stream,
|
||||
once: false,
|
||||
|
|
@ -214,12 +213,7 @@ void main() {
|
|||
late UuidValue sharedScopeId;
|
||||
|
||||
setUp(() async {
|
||||
sharedScopeId = const Uuid().v7obj();
|
||||
await _upsertScopeMembership(
|
||||
rawServerSession,
|
||||
userUuid: testCrdtUserId,
|
||||
scopeUuid: sharedScopeId,
|
||||
);
|
||||
sharedScopeId = await rawServerSession.crdt.scopes.createFor(testCrdtUserId);
|
||||
});
|
||||
|
||||
group('and personal and shared scope rows exist on the server,', () {
|
||||
|
|
@ -313,11 +307,8 @@ void main() {
|
|||
final syncSession = testClient.crdt.syncContinuously(clientSession);
|
||||
addTearDown(syncSession.cancel);
|
||||
|
||||
final laterScopeId = const Uuid().v7obj();
|
||||
await _upsertScopeMembership(
|
||||
rawServerSession,
|
||||
userUuid: testCrdtUserId,
|
||||
scopeUuid: laterScopeId,
|
||||
final laterScopeId = await rawServerSession.crdt.scopes.createFor(
|
||||
testCrdtUserId,
|
||||
);
|
||||
|
||||
final sharedPerson = await serverSession.db.transactionForUser(
|
||||
|
|
@ -363,10 +354,9 @@ void main() {
|
|||
null;
|
||||
});
|
||||
|
||||
await _revokeScopeMembership(
|
||||
rawServerSession,
|
||||
userUuid: testCrdtUserId,
|
||||
scopeUuid: sharedScopeId,
|
||||
await rawServerSession.crdt.scopes.revoke(
|
||||
scope: sharedScopeId,
|
||||
user: testCrdtUserId,
|
||||
);
|
||||
|
||||
// The revoked scope's rows disappear from membership-wide reads…
|
||||
|
|
@ -466,12 +456,8 @@ void main() {
|
|||
late UuidValue serverPersonId;
|
||||
|
||||
setUp(() async {
|
||||
readOnlyScopeId = const Uuid().v7obj();
|
||||
await _upsertScopeMembership(
|
||||
rawServerSession,
|
||||
userUuid: testCrdtUserId,
|
||||
scopeUuid: readOnlyScopeId,
|
||||
role: CrdtScopeRole.readOnly,
|
||||
readOnlyScopeId = await rawServerSession.crdt.scopes.create(
|
||||
grants: {testCrdtUserId: CrdtScopeRole.readOnly},
|
||||
);
|
||||
final serverPerson = await serverSession.db.transactionForUser(
|
||||
readOnlyScopeId,
|
||||
|
|
@ -570,10 +556,9 @@ void main() {
|
|||
);
|
||||
pendingPersonId = pendingPerson.id!;
|
||||
|
||||
await _upsertScopeMembership(
|
||||
rawServerSession,
|
||||
userUuid: testCrdtUserId,
|
||||
scopeUuid: sharedScopeId,
|
||||
await rawServerSession.crdt.scopes.grant(
|
||||
scope: sharedScopeId,
|
||||
user: testCrdtUserId,
|
||||
role: CrdtScopeRole.readOnly,
|
||||
);
|
||||
final inboundPerson = await serverSession.db.transactionForUser(
|
||||
|
|
@ -648,10 +633,9 @@ void main() {
|
|||
);
|
||||
revokedPersonId = revokedPerson.id!;
|
||||
await testClient.crdt.syncOnce(clientSession);
|
||||
await _revokeScopeMembership(
|
||||
rawServerSession,
|
||||
userUuid: testCrdtUserId,
|
||||
scopeUuid: sharedScopeId,
|
||||
await rawServerSession.crdt.scopes.revoke(
|
||||
scope: sharedScopeId,
|
||||
user: testCrdtUserId,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -727,10 +711,7 @@ void main() {
|
|||
late UuidValue ungrantedScopeId;
|
||||
|
||||
setUp(() async {
|
||||
ungrantedScopeId = const Uuid().v7obj();
|
||||
await CrdtScopeManager(rawServerSession).getOrCreate(
|
||||
ungrantedScopeId,
|
||||
);
|
||||
ungrantedScopeId = await rawServerSession.crdt.scopes.create();
|
||||
});
|
||||
|
||||
test('then it is rejected before writing.', () async {
|
||||
|
|
@ -1300,16 +1281,11 @@ void main() {
|
|||
);
|
||||
await serverSession.db.initialize();
|
||||
|
||||
sharedScopeId = const Uuid().v7obj();
|
||||
await _upsertScopeMembership(
|
||||
rawServerSession,
|
||||
userUuid: testCrdtUserId,
|
||||
scopeUuid: sharedScopeId,
|
||||
);
|
||||
await _upsertScopeMembership(
|
||||
rawServerSession,
|
||||
userUuid: secondUserId,
|
||||
scopeUuid: sharedScopeId,
|
||||
sharedScopeId = await rawServerSession.crdt.scopes.create(
|
||||
grants: {
|
||||
testCrdtUserId: CrdtScopeRole.readWrite,
|
||||
secondUserId: CrdtScopeRole.readWrite,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -1595,39 +1571,6 @@ Future<void> _waitUntil(
|
|||
throw TimeoutException('Condition was not met within $timeout.');
|
||||
}
|
||||
|
||||
/// Upserts a `crdt_scope_members` row: an authoritative grant on the server,
|
||||
/// or a (possibly stale) locally projected grant on a client session.
|
||||
Future<void> _upsertScopeMembership(
|
||||
DatabaseSession session, {
|
||||
required UuidValue userUuid,
|
||||
required UuidValue scopeUuid,
|
||||
CrdtScopeRole role = CrdtScopeRole.readWrite,
|
||||
}) async {
|
||||
final scope = await CrdtScopeManager(session).getOrCreate(scopeUuid);
|
||||
final encodedScopeId = ValueEncoder.instance.convert(scope.id);
|
||||
final encodedUserUuid = ValueEncoder.instance.convert(userUuid);
|
||||
final encodedRole = ValueEncoder.instance.convert(role.toJson());
|
||||
await session.db.unsafeExecute(
|
||||
'INSERT INTO "crdt_scope_members" ("scopeId", "userUuid", "role") '
|
||||
'VALUES ($encodedScopeId, $encodedUserUuid, $encodedRole) '
|
||||
'ON CONFLICT ("scopeId", "userUuid") DO UPDATE SET "role" = $encodedRole',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _revokeScopeMembership(
|
||||
DatabaseSession session, {
|
||||
required UuidValue userUuid,
|
||||
required UuidValue scopeUuid,
|
||||
}) async {
|
||||
final scope = await CrdtScopeManager(session).getOrCreate(scopeUuid);
|
||||
final encodedScopeId = ValueEncoder.instance.convert(scope.id);
|
||||
final encodedUserUuid = ValueEncoder.instance.convert(userUuid);
|
||||
await session.db.unsafeExecute(
|
||||
'DELETE FROM "crdt_scope_members" '
|
||||
'WHERE "scopeId" = $encodedScopeId AND "userUuid" = $encodedUserUuid',
|
||||
);
|
||||
}
|
||||
|
||||
class TestClientAuthKeyProvider implements ClientAuthKeyProvider {
|
||||
TestClientAuthKeyProvider([this.userKey = 'token']);
|
||||
|
||||
|
|
|
|||
|
|
@ -80,12 +80,8 @@ void main() {
|
|||
late Object? sessionError;
|
||||
|
||||
setUp(() async {
|
||||
sharedScopeId = const Uuid().v7obj();
|
||||
await _upsertScopeMembership(
|
||||
rawServerSession,
|
||||
userUuid: testCrdtUserId,
|
||||
scopeUuid: sharedScopeId,
|
||||
role: CrdtScopeRole.readOnly,
|
||||
sharedScopeId = await rawServerSession.crdt.scopes.create(
|
||||
grants: {testCrdtUserId: CrdtScopeRole.readOnly},
|
||||
);
|
||||
await _upsertScopeMembership(
|
||||
clientSession,
|
||||
|
|
@ -137,7 +133,6 @@ void main() {
|
|||
|
||||
sessionError = await _syncOnceWithSplicedMergeChunk(
|
||||
serverSync: rawServerSession.crdt,
|
||||
serverSession: serverSession,
|
||||
clientSync: clientSync,
|
||||
clientSession: clientSession,
|
||||
userUuid: testCrdtUserId,
|
||||
|
|
@ -221,7 +216,6 @@ void main() {
|
|||
|
||||
sessionError = await _syncOnceWithSplicedMergeChunk(
|
||||
serverSync: rawServerSession.crdt,
|
||||
serverSession: serverSession,
|
||||
clientSync: clientSync,
|
||||
clientSession: clientSession,
|
||||
userUuid: testCrdtUserId,
|
||||
|
|
@ -266,8 +260,7 @@ void main() {
|
|||
/// Returns the error that ended the authoritative session, or null when it
|
||||
/// closed cleanly.
|
||||
Future<Object?> _syncOnceWithSplicedMergeChunk({
|
||||
required CrdtSync serverSync,
|
||||
required DatabaseSession serverSession,
|
||||
required CrdtSession serverSync,
|
||||
required CrdtSync clientSync,
|
||||
required DatabaseSession clientSession,
|
||||
required UuidValue userUuid,
|
||||
|
|
@ -318,7 +311,6 @@ Future<Object?> _syncOnceWithSplicedMergeChunk({
|
|||
|
||||
final serverSubscription = serverSync
|
||||
.sync(
|
||||
serverSession,
|
||||
userId: userUuid,
|
||||
inbound: clientToServer.stream,
|
||||
once: true,
|
||||
|
|
@ -349,8 +341,8 @@ Future<Object?> _syncOnceWithSplicedMergeChunk({
|
|||
}
|
||||
}
|
||||
|
||||
/// Upserts a `crdt_scope_members` row: an authoritative grant on the server,
|
||||
/// or a (possibly stale) locally projected grant on a client session.
|
||||
/// Upserts a projected `crdt_scope_members` row for stale or adversarial client
|
||||
/// state.
|
||||
Future<void> _upsertScopeMembership(
|
||||
DatabaseSession session, {
|
||||
required UuidValue userUuid,
|
||||
|
|
|
|||
Loading…
Reference in a new issue