refactor!: Rename sync APIs and ownership spaces (#128)
This commit is contained in:
parent
4331daf661
commit
b89725f6ea
18 changed files with 43650 additions and 9417 deletions
|
|
@ -1,7 +1,7 @@
|
|||
export 'package:serverpod_offline_sync/serverpod_offline_sync.dart' hide Protocol;
|
||||
|
||||
export 'src/business/crdt_scopes.dart';
|
||||
export 'src/business/crdt_sync.dart';
|
||||
export 'src/endpoints/crdt_sync_endpoint.dart';
|
||||
export 'src/business/offline_sync.dart';
|
||||
export 'src/business/offline_sync_spaces.dart';
|
||||
export 'src/endpoints/offline_sync_endpoint.dart';
|
||||
export 'src/generated/endpoints.dart';
|
||||
export 'src/generated/protocol.dart';
|
||||
|
|
|
|||
|
|
@ -1,158 +0,0 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync/serverpod_offline_sync.dart';
|
||||
|
||||
/// Server-side service for managing CRDT shared-scope membership.
|
||||
///
|
||||
/// This is a data-invariant service, not an authorization policy. Applications
|
||||
/// call it from their own endpoints after applying their own access checks.
|
||||
class CrdtScopes {
|
||||
/// Creates a session-bound CRDT scope management service.
|
||||
CrdtScopes(this._session);
|
||||
|
||||
final Session _session;
|
||||
|
||||
/// Creates a shared scope and returns its UUID.
|
||||
///
|
||||
/// [grants], if given, adds each user as a member with its role, applied in
|
||||
/// one transaction with the scope insert. With no grants the scope is created
|
||||
/// dormant — it exists but no one can sync it until membership is granted.
|
||||
Future<UuidValue> create({
|
||||
Map<UuidValue, CrdtScopeRole>? grants,
|
||||
Transaction? transaction,
|
||||
}) async {
|
||||
final scope = const Uuid().v7obj();
|
||||
await DatabaseUtil.runInTransactionOrSavepoint(
|
||||
_session.db,
|
||||
transaction,
|
||||
(tx) async {
|
||||
final insertedScope = await CrdtScope.db.insertRow(
|
||||
_session,
|
||||
CrdtScope(uuidScopeId: scope),
|
||||
transaction: tx,
|
||||
);
|
||||
await _applyGrants(insertedScope.id!, grants ?? const {}, tx);
|
||||
},
|
||||
);
|
||||
return scope;
|
||||
}
|
||||
|
||||
/// Creates a shared scope granting [user] the given [role], in one transaction.
|
||||
Future<UuidValue> createFor(
|
||||
UuidValue user, {
|
||||
CrdtScopeRole role = CrdtScopeRole.readWrite,
|
||||
Transaction? transaction,
|
||||
}) => create(grants: {user: role}, transaction: transaction);
|
||||
|
||||
/// Grants [user] the given [role] in [scope], upserting an existing membership.
|
||||
Future<void> grant({
|
||||
required UuidValue scope,
|
||||
required UuidValue user,
|
||||
required CrdtScopeRole role,
|
||||
Transaction? transaction,
|
||||
}) => grantAll(scope, {user: role}, transaction: transaction);
|
||||
|
||||
/// Grants several members in one transaction; the bulk twin of [grant].
|
||||
Future<void> grantAll(
|
||||
UuidValue scope,
|
||||
Map<UuidValue, CrdtScopeRole> grants, {
|
||||
Transaction? transaction,
|
||||
}) {
|
||||
return DatabaseUtil.runInTransactionOrSavepoint(
|
||||
_session.db,
|
||||
transaction,
|
||||
(tx) async {
|
||||
final scopeRow = await CrdtScope.db.findFirstRow(
|
||||
_session,
|
||||
where: (t) => t.uuidScopeId.equals(scope),
|
||||
transaction: tx,
|
||||
);
|
||||
final scopeId = scopeRow?.id;
|
||||
if (scopeId == null) {
|
||||
throw CrdtScopeNotFoundException(scope);
|
||||
}
|
||||
await _applyGrants(scopeId, grants, tx);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Removes [user]'s membership in [scope]. A no-op if not a member.
|
||||
Future<void> revoke({
|
||||
required UuidValue scope,
|
||||
required UuidValue user,
|
||||
Transaction? transaction,
|
||||
}) async {
|
||||
await CrdtScopeMember.db.deleteWhere(
|
||||
_session,
|
||||
where: (t) => t.userUuid.equals(user) & t.scope.uuidScopeId.equals(scope),
|
||||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// The role [user] holds in [scope], or null if not a member.
|
||||
Future<CrdtScopeRole?> roleOf({
|
||||
required UuidValue user,
|
||||
required UuidValue scope,
|
||||
Transaction? transaction,
|
||||
}) {
|
||||
return CrdtScopeMembership.roleOf(
|
||||
_session,
|
||||
userUuid: user,
|
||||
scopeUuid: scope,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// The members of [scope] and their roles.
|
||||
Future<Map<UuidValue, CrdtScopeRole>> members(
|
||||
UuidValue scope, {
|
||||
Transaction? transaction,
|
||||
}) async {
|
||||
final memberships = await CrdtScopeMember.db.find(
|
||||
_session,
|
||||
where: (t) => t.scope.uuidScopeId.equals(scope),
|
||||
transaction: transaction,
|
||||
);
|
||||
|
||||
return {
|
||||
for (final membership in memberships) membership.userUuid: membership.role,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _applyGrants(
|
||||
int scopeId,
|
||||
Map<UuidValue, CrdtScopeRole> grants,
|
||||
Transaction transaction,
|
||||
) async {
|
||||
if (grants.isEmpty) return;
|
||||
|
||||
await CrdtScopeMember.db.upsert(
|
||||
_session,
|
||||
[
|
||||
for (final grant in grants.entries)
|
||||
CrdtScopeMember(
|
||||
scopeId: scopeId,
|
||||
userUuid: grant.key,
|
||||
role: grant.value,
|
||||
),
|
||||
],
|
||||
conflictColumns: (t) => [t.userUuid, t.scopeId],
|
||||
updateColumns: (t) => [t.role],
|
||||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown when a scope-management operation targets a missing CRDT scope row.
|
||||
final class CrdtScopeNotFoundException implements Exception {
|
||||
/// Creates a [CrdtScopeNotFoundException].
|
||||
const CrdtScopeNotFoundException(this.scope);
|
||||
|
||||
/// The scope UUID that could not be found.
|
||||
final UuidValue scope;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'CrdtScopeNotFoundException: no CRDT scope row exists for "$scope".';
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync/serverpod_offline_sync.dart';
|
||||
|
||||
import 'offline_sync_spaces.dart';
|
||||
|
||||
/// The CRDT sync configured per [Serverpod] instance.
|
||||
///
|
||||
/// Keyed by the [Serverpod] instance so each pod owns its own [OfflineSyncEngine] (and
|
||||
/// the [OfflineSyncDatabaseContext] it carries) instead of sharing a single
|
||||
/// process-wide singleton.
|
||||
final _offlineSyncByServerpod = Expando<OfflineSyncEngine>('offlineSync');
|
||||
|
||||
/// Intercepts each Serverpod session database with a CRDT-aware database once
|
||||
/// [OfflineSyncInitialize.initializeOfflineSync] has configured sync.
|
||||
///
|
||||
/// When sync has not been configured for the session's [Serverpod], the
|
||||
/// original [inner] database is returned unchanged.
|
||||
Database offlineSyncDatabaseInterceptor(Session session, Database inner) {
|
||||
final offlineSync = _offlineSyncByServerpod[session.server.serverpod];
|
||||
return offlineSync?.wrapDatabase(inner) ?? inner;
|
||||
}
|
||||
|
||||
/// Extension methods for [Serverpod] to configure the CRDT sync on the server.
|
||||
extension OfflineSyncInitialize on Serverpod {
|
||||
/// Configures the CRDT sync with the given sync tables.
|
||||
///
|
||||
/// Must be called during server startup before any sync requests are made.
|
||||
/// Will override any previous initialization for this [Serverpod] instance.
|
||||
///
|
||||
/// The [Serverpod] instance must be constructed with [offlineSyncDatabaseInterceptor]
|
||||
/// as its `databaseInterceptor`. Otherwise each session's [Session.db] stays a
|
||||
/// plain database and server-side ORM mutations on synced tables are not
|
||||
/// CRDT-tracked.
|
||||
///
|
||||
/// [syncBatchSize] controls the maximum number of merge changes carried by
|
||||
/// each sync stream chunk.
|
||||
///
|
||||
/// [continuousSyncInterval] controls how long a continuous sync session waits
|
||||
/// after completing one sync round before checking for local changes again.
|
||||
void initializeOfflineSync({
|
||||
required List<Table> syncTables,
|
||||
int syncBatchSize = OfflineSyncEngine.defaultSyncBatchSize,
|
||||
Duration continuousSyncInterval = OfflineSyncEngine.defaultContinuousSyncInterval,
|
||||
}) {
|
||||
_offlineSyncByServerpod[this] = OfflineSyncEngine(
|
||||
syncTables: syncTables,
|
||||
serializationManager: serializationManager,
|
||||
syncBatchSize: syncBatchSize,
|
||||
continuousSyncInterval: continuousSyncInterval,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Session-bound CRDT services configured for a [Serverpod] instance.
|
||||
///
|
||||
/// This facade is ephemeral: each `Session.offlineSync` access creates a small wrapper
|
||||
/// around the shared [OfflineSyncEngine] instance and the current [Session].
|
||||
class OfflineSyncSession {
|
||||
/// Creates CRDT services bound to a session.
|
||||
OfflineSyncSession(this._session, this._sync);
|
||||
|
||||
final Session _session;
|
||||
final OfflineSyncEngine _sync;
|
||||
|
||||
/// Returns the server-side space management service.
|
||||
OfflineSyncSpaces get spaces => OfflineSyncSpaces(_session);
|
||||
|
||||
/// Runs a CRDT sync session with this [OfflineSyncSession]'s [Session] bound.
|
||||
Stream<OfflineSyncStreamEvent> sync({
|
||||
required UuidValue userId,
|
||||
required Stream<OfflineSyncStreamEvent> inbound,
|
||||
required OfflineSyncPeerMode mode,
|
||||
bool once = false,
|
||||
OfflineSyncOnMergeSuccess? onMergeSuccess,
|
||||
}) {
|
||||
return _sync.sync(
|
||||
_session,
|
||||
userId: userId,
|
||||
inbound: inbound,
|
||||
once: once,
|
||||
mode: mode,
|
||||
onMergeSuccess: onMergeSuccess,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to access CRDT services for [Session] from the [Serverpod] instance.
|
||||
extension OfflineSyncSessionExtension on Session {
|
||||
/// Returns the CRDT services configured for this session.
|
||||
OfflineSyncSession get offlineSync {
|
||||
final sync = _offlineSyncByServerpod[server.serverpod];
|
||||
if (sync == null) {
|
||||
throw StateError(
|
||||
'The OfflineSyncEngine has not been initialized for this Serverpod instance. '
|
||||
'Call pod.initializeOfflineSync(...) during server startup to configure '
|
||||
'the CRDT sync.',
|
||||
);
|
||||
}
|
||||
return OfflineSyncSession(this, sync);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync/serverpod_offline_sync.dart';
|
||||
|
||||
/// Server-side service for managing CRDT shared-space membership.
|
||||
///
|
||||
/// This is a data-invariant service, not an authorization policy. Applications
|
||||
/// call it from their own endpoints after applying their own access checks.
|
||||
class OfflineSyncSpaces {
|
||||
/// Creates a session-bound CRDT space management service.
|
||||
OfflineSyncSpaces(this._session);
|
||||
|
||||
final Session _session;
|
||||
|
||||
/// Creates a shared space and returns its UUID.
|
||||
///
|
||||
/// [grants], if given, adds each user as a member with its role, applied in
|
||||
/// one transaction with the space insert. With no grants the space is created
|
||||
/// dormant — it exists but no one can sync it until membership is granted.
|
||||
Future<UuidValue> create({
|
||||
Map<UuidValue, OfflineSyncSpaceRole>? grants,
|
||||
Transaction? transaction,
|
||||
}) async {
|
||||
final space = const Uuid().v7obj();
|
||||
await DatabaseUtil.runInTransactionOrSavepoint(
|
||||
_session.db,
|
||||
transaction,
|
||||
(tx) async {
|
||||
final insertedSpace = await OfflineSyncSpace.db.insertRow(
|
||||
_session,
|
||||
OfflineSyncSpace(uuidSpaceId: space),
|
||||
transaction: tx,
|
||||
);
|
||||
await _applyGrants(insertedSpace.id!, grants ?? const {}, tx);
|
||||
},
|
||||
);
|
||||
return space;
|
||||
}
|
||||
|
||||
/// Creates a shared space granting [user] the given [role], in one transaction.
|
||||
Future<UuidValue> createFor(
|
||||
UuidValue user, {
|
||||
OfflineSyncSpaceRole role = OfflineSyncSpaceRole.readWrite,
|
||||
Transaction? transaction,
|
||||
}) => create(grants: {user: role}, transaction: transaction);
|
||||
|
||||
/// Grants [user] the given [role] in [space], upserting an existing membership.
|
||||
Future<void> grant({
|
||||
required UuidValue space,
|
||||
required UuidValue user,
|
||||
required OfflineSyncSpaceRole role,
|
||||
Transaction? transaction,
|
||||
}) => grantAll(space, {user: role}, transaction: transaction);
|
||||
|
||||
/// Grants several members in one transaction; the bulk twin of [grant].
|
||||
Future<void> grantAll(
|
||||
UuidValue space,
|
||||
Map<UuidValue, OfflineSyncSpaceRole> grants, {
|
||||
Transaction? transaction,
|
||||
}) {
|
||||
return DatabaseUtil.runInTransactionOrSavepoint(
|
||||
_session.db,
|
||||
transaction,
|
||||
(tx) async {
|
||||
final spaceRow = await OfflineSyncSpace.db.findFirstRow(
|
||||
_session,
|
||||
where: (t) => t.uuidSpaceId.equals(space),
|
||||
transaction: tx,
|
||||
);
|
||||
final spaceId = spaceRow?.id;
|
||||
if (spaceId == null) {
|
||||
throw OfflineSyncSpaceNotFoundException(space);
|
||||
}
|
||||
await _applyGrants(spaceId, grants, tx);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Removes [user]'s membership in [space]. A no-op if not a member.
|
||||
Future<void> revoke({
|
||||
required UuidValue space,
|
||||
required UuidValue user,
|
||||
Transaction? transaction,
|
||||
}) async {
|
||||
await OfflineSyncSpaceMember.db.deleteWhere(
|
||||
_session,
|
||||
where: (t) => t.userUuid.equals(user) & t.space.uuidSpaceId.equals(space),
|
||||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// The role [user] holds in [space], or null if not a member.
|
||||
Future<OfflineSyncSpaceRole?> roleOf({
|
||||
required UuidValue user,
|
||||
required UuidValue space,
|
||||
Transaction? transaction,
|
||||
}) {
|
||||
return OfflineSyncSpaceMembership.roleOf(
|
||||
_session,
|
||||
userUuid: user,
|
||||
spaceUuid: space,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// The members of [space] and their roles.
|
||||
Future<Map<UuidValue, OfflineSyncSpaceRole>> members(
|
||||
UuidValue space, {
|
||||
Transaction? transaction,
|
||||
}) async {
|
||||
final memberships = await OfflineSyncSpaceMember.db.find(
|
||||
_session,
|
||||
where: (t) => t.space.uuidSpaceId.equals(space),
|
||||
transaction: transaction,
|
||||
);
|
||||
|
||||
return {
|
||||
for (final membership in memberships) membership.userUuid: membership.role,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _applyGrants(
|
||||
int spaceId,
|
||||
Map<UuidValue, OfflineSyncSpaceRole> grants,
|
||||
Transaction transaction,
|
||||
) async {
|
||||
if (grants.isEmpty) return;
|
||||
|
||||
await OfflineSyncSpaceMember.db.upsert(
|
||||
_session,
|
||||
[
|
||||
for (final grant in grants.entries)
|
||||
OfflineSyncSpaceMember(
|
||||
spaceId: spaceId,
|
||||
userUuid: grant.key,
|
||||
role: grant.value,
|
||||
),
|
||||
],
|
||||
conflictColumns: (t) => [t.userUuid, t.spaceId],
|
||||
updateColumns: (t) => [t.role],
|
||||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown when a space-management operation targets a missing CRDT space row.
|
||||
final class OfflineSyncSpaceNotFoundException implements Exception {
|
||||
/// Creates a [OfflineSyncSpaceNotFoundException].
|
||||
const OfflineSyncSpaceNotFoundException(this.space);
|
||||
|
||||
/// The space UUID that could not be found.
|
||||
final UuidValue space;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'OfflineSyncSpaceNotFoundException: no CRDT space row exists for "$space".';
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync/serverpod_offline_sync.dart';
|
||||
|
||||
import '../business/offline_sync.dart';
|
||||
|
||||
/// Endpoint for CRDT-based offline-first synchronization.
|
||||
class OfflineSyncEndpoint extends Endpoint {
|
||||
@override
|
||||
bool get requireLogin => true;
|
||||
|
||||
/// Opens a bidirectional CRDT sync session with the authenticated client.
|
||||
Stream<OfflineSyncStreamEvent> sync(
|
||||
Session session, {
|
||||
required Stream<OfflineSyncStreamEvent> changes,
|
||||
bool once = false,
|
||||
}) async* {
|
||||
yield* session.offlineSync.sync(
|
||||
userId: UuidValue.withValidation(session.authenticated!.userIdentifier),
|
||||
inbound: changes,
|
||||
once: once,
|
||||
mode: OfflineSyncPeerMode.authoritative,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,22 +13,22 @@
|
|||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import 'package:serverpod_offline_sync/serverpod_offline_sync.dart'
|
||||
as _icw2tu00;
|
||||
import '../endpoints/crdt_sync_endpoint.dart' as _i87mlr2k;
|
||||
import '../endpoints/offline_sync_endpoint.dart' as _i3x2h4cf;
|
||||
|
||||
class Endpoints extends _is.EndpointDispatch {
|
||||
@override
|
||||
void initializeEndpoints(_is.Server server) {
|
||||
var endpoints = <String, _is.Endpoint>{
|
||||
'crdtSync': _i87mlr2k.CrdtSyncEndpoint()
|
||||
'offlineSync': _i3x2h4cf.OfflineSyncEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'crdtSync',
|
||||
'offlineSync',
|
||||
'serverpod_offline_sync',
|
||||
),
|
||||
};
|
||||
connectors['crdtSync'] = _is.EndpointConnector(
|
||||
name: 'crdtSync',
|
||||
endpoint: endpoints['crdtSync']!,
|
||||
connectors['offlineSync'] = _is.EndpointConnector(
|
||||
name: 'offlineSync',
|
||||
endpoint: endpoints['offlineSync']!,
|
||||
methodConnectors: {
|
||||
'sync': _is.MethodStreamConnector(
|
||||
name: 'sync',
|
||||
|
|
@ -41,7 +41,9 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
},
|
||||
streamParams: {
|
||||
'changes':
|
||||
_is.StreamParameterDescription<_icw2tu00.CrdtSyncStreamEvent>(
|
||||
_is.StreamParameterDescription<
|
||||
_icw2tu00.OfflineSyncStreamEvent
|
||||
>(
|
||||
name: 'changes',
|
||||
nullable: false,
|
||||
),
|
||||
|
|
@ -52,12 +54,13 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
Map<String, Stream> streamParams,
|
||||
) => (endpoints['crdtSync'] as _i87mlr2k.CrdtSyncEndpoint).sync(
|
||||
session,
|
||||
changes: streamParams['changes']!
|
||||
.cast<_icw2tu00.CrdtSyncStreamEvent>(),
|
||||
once: params['once'],
|
||||
),
|
||||
) => (endpoints['offlineSync'] as _i3x2h4cf.OfflineSyncEndpoint)
|
||||
.sync(
|
||||
session,
|
||||
changes: streamParams['changes']!
|
||||
.cast<_icw2tu00.OfflineSyncStreamEvent>(),
|
||||
once: params['once'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
crdtSync:
|
||||
offlineSync:
|
||||
- sync:
|
||||
|
|
|
|||
2627
packages/serverpod_offline_sync_server/migrations/20260914143806119/definition.json
generated
Normal file
2627
packages/serverpod_offline_sync_server/migrations/20260914143806119/definition.json
generated
Normal file
File diff suppressed because it is too large
Load diff
535
packages/serverpod_offline_sync_server/migrations/20260914143806119/definition.sql
generated
Normal file
535
packages/serverpod_offline_sync_server/migrations/20260914143806119/definition.sql
generated
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
BEGIN;
|
||||
|
||||
--
|
||||
-- Class CrdtDataAttemptedValue as table crdt_data_attempted_value
|
||||
--
|
||||
CREATE TABLE "crdt_data_attempted_value" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"fieldId" INTEGER NOT NULL,
|
||||
"value" BLOB NOT NULL,
|
||||
"projectionReason" INTEGER NOT NULL,
|
||||
CONSTRAINT "crdt_data_attempted_value_fk_0" FOREIGN KEY ("fieldId") REFERENCES "crdt_data_fields" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "crdt_data_attempted_value__fieldId__unique_idx" ON "crdt_data_attempted_value" ("fieldId");
|
||||
|
||||
|
||||
--
|
||||
-- Class CrdtDataField as table crdt_data_fields
|
||||
--
|
||||
CREATE TABLE "crdt_data_fields" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"hlcDatetime" INTEGER NOT NULL,
|
||||
"hlcCounter" INTEGER NOT NULL,
|
||||
"rowId" INTEGER NOT NULL,
|
||||
"columnId" INTEGER NOT NULL,
|
||||
"nodeId" INTEGER NOT NULL,
|
||||
CONSTRAINT "crdt_data_fields_fk_0" FOREIGN KEY ("rowId") REFERENCES "crdt_data_rows" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "crdt_data_fields_fk_1" FOREIGN KEY ("columnId") REFERENCES "crdt_schema_columns" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "crdt_data_fields_fk_2" FOREIGN KEY ("nodeId") REFERENCES "crdt_nodes" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "crdt_data_fields_row_column_idx" ON "crdt_data_fields" ("rowId", "columnId");
|
||||
|
||||
|
||||
--
|
||||
-- Class CrdtDataRow as table crdt_data_rows
|
||||
--
|
||||
CREATE TABLE "crdt_data_rows" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"hlcDatetime" INTEGER NOT NULL,
|
||||
"hlcCounter" INTEGER NOT NULL,
|
||||
"spaceId" INTEGER NOT NULL,
|
||||
"tblId" INTEGER NOT NULL,
|
||||
"uuidRowId" BLOB NOT NULL,
|
||||
"nodeId" INTEGER NOT NULL,
|
||||
"visibility" INTEGER NOT NULL DEFAULT (0),
|
||||
CONSTRAINT "crdt_data_rows_fk_0" FOREIGN KEY ("spaceId") REFERENCES "offline_sync_spaces" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "crdt_data_rows_fk_1" FOREIGN KEY ("tblId") REFERENCES "crdt_schema_tables" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "crdt_data_rows_fk_2" FOREIGN KEY ("nodeId") REFERENCES "crdt_nodes" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "crdt_data_rows_space_tbl_row_idx" ON "crdt_data_rows" ("spaceId", "tblId", "uuidRowId");
|
||||
|
||||
|
||||
--
|
||||
-- Class CrdtDataDeleted as table crdt_data_tombstone
|
||||
--
|
||||
CREATE TABLE "crdt_data_tombstone" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"hlcDatetime" INTEGER NOT NULL,
|
||||
"hlcCounter" INTEGER NOT NULL,
|
||||
"rowId" INTEGER NOT NULL,
|
||||
"nodeId" INTEGER NOT NULL,
|
||||
"clFlag" INTEGER NOT NULL,
|
||||
"reason" INTEGER NOT NULL,
|
||||
CONSTRAINT "crdt_data_tombstone_fk_0" FOREIGN KEY ("rowId") REFERENCES "crdt_data_rows" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "crdt_data_tombstone_fk_1" FOREIGN KEY ("nodeId") REFERENCES "crdt_nodes" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "crdt_data_tombstone_row_idx" ON "crdt_data_tombstone" ("rowId");
|
||||
|
||||
|
||||
--
|
||||
-- Class CrdtNode as table crdt_nodes
|
||||
--
|
||||
CREATE TABLE "crdt_nodes" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"uuidNodeId" BLOB NOT NULL DEFAULT (unhex(printf('%012x', CAST(unixepoch('now', 'subsecond') * 1000 AS INTEGER)) || '7' || substr(hex(randomblob(2)), 2, 3) || substr('89AB', 1 + (abs(random()) % 4), 1) || substr(hex(randomblob(8)), 2, 15))),
|
||||
"lastHlc" BLOB
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "crdt_nodes__uuidNodeId__unique_idx" ON "crdt_nodes" ("uuidNodeId");
|
||||
|
||||
|
||||
--
|
||||
-- Class CrdtSchemaColumn as table crdt_schema_columns
|
||||
--
|
||||
CREATE TABLE "crdt_schema_columns" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"tblId" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"columnType" TEXT NOT NULL,
|
||||
"dartType" TEXT NOT NULL,
|
||||
"isNullable" INTEGER NOT NULL,
|
||||
CONSTRAINT "crdt_schema_columns_fk_0" FOREIGN KEY ("tblId") REFERENCES "crdt_schema_tables" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "crdt_schema_columns_table_column_idx" ON "crdt_schema_columns" ("tblId", "name");
|
||||
|
||||
|
||||
--
|
||||
-- Class CrdtSchemaTable as table crdt_schema_tables
|
||||
--
|
||||
CREATE TABLE "crdt_schema_tables" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"name" TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "crdt_schema_tables__name__unique_idx" ON "crdt_schema_tables" ("name");
|
||||
|
||||
|
||||
--
|
||||
-- Class OfflineSyncIntegrityViolation as table offline_sync_integrity_violations
|
||||
--
|
||||
CREATE TABLE "offline_sync_integrity_violations" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"type" TEXT NOT NULL,
|
||||
"domainTableName" TEXT NOT NULL,
|
||||
"uuidRowId" BLOB NOT NULL,
|
||||
"ownerSpaceUuid" BLOB,
|
||||
"incomingSpaceUuid" BLOB NOT NULL,
|
||||
"operation" TEXT NOT NULL,
|
||||
"uuidNodeId" BLOB,
|
||||
"crdtDataRowId" INTEGER,
|
||||
"hlcDatetime" INTEGER,
|
||||
"hlcCounter" INTEGER,
|
||||
"firstSeenAt" INTEGER NOT NULL,
|
||||
"lastSeenAt" INTEGER NOT NULL,
|
||||
"occurrences" INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "offline_sync_integrity_violations_key_idx" ON "offline_sync_integrity_violations" ("type", "operation", "domainTableName", "uuidRowId", "ownerSpaceUuid", "incomingSpaceUuid");
|
||||
|
||||
|
||||
--
|
||||
-- Class OfflineSyncSpaceMember as table offline_sync_space_members
|
||||
--
|
||||
CREATE TABLE "offline_sync_space_members" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"spaceId" INTEGER NOT NULL,
|
||||
"userUuid" BLOB NOT NULL,
|
||||
"role" TEXT NOT NULL,
|
||||
CONSTRAINT "offline_sync_space_members_fk_0" FOREIGN KEY ("spaceId") REFERENCES "offline_sync_spaces" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "offline_sync_space_member_unique_idx" ON "offline_sync_space_members" ("userUuid", "spaceId");
|
||||
|
||||
|
||||
--
|
||||
-- Class OfflineSyncSpaceNode as table offline_sync_space_nodes
|
||||
--
|
||||
CREATE TABLE "offline_sync_space_nodes" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"spaceId" INTEGER NOT NULL,
|
||||
"nodeId" INTEGER NOT NULL,
|
||||
"lastReceivedHlc" BLOB,
|
||||
CONSTRAINT "offline_sync_space_nodes_fk_0" FOREIGN KEY ("spaceId") REFERENCES "offline_sync_spaces" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "offline_sync_space_nodes_fk_1" FOREIGN KEY ("nodeId") REFERENCES "crdt_nodes" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "offline_sync_space_node_unique_idx" ON "offline_sync_space_nodes" ("spaceId", "nodeId");
|
||||
|
||||
|
||||
--
|
||||
-- Class OfflineSyncSpace as table offline_sync_spaces
|
||||
--
|
||||
CREATE TABLE "offline_sync_spaces" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"uuidSpaceId" BLOB NOT NULL DEFAULT (unhex(printf('%012x', CAST(unixepoch('now', 'subsecond') * 1000 AS INTEGER)) || '7' || substr(hex(randomblob(2)), 2, 3) || substr('89AB', 1 + (abs(random()) % 4), 1) || substr(hex(randomblob(8)), 2, 15))),
|
||||
"currentNodeId" INTEGER,
|
||||
CONSTRAINT "offline_sync_spaces_fk_0" FOREIGN KEY ("currentNodeId") REFERENCES "crdt_nodes" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "offline_sync_spaces__uuidSpaceId__unique_idx" ON "offline_sync_spaces" ("uuidSpaceId");
|
||||
|
||||
|
||||
--
|
||||
-- Class CloudStorageEntry as table serverpod_cloud_storage
|
||||
--
|
||||
CREATE TABLE "serverpod_cloud_storage" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"storageId" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"addedTime" INTEGER NOT NULL,
|
||||
"expiration" INTEGER,
|
||||
"byteData" BLOB NOT NULL,
|
||||
"verified" INTEGER NOT NULL,
|
||||
"contentType" TEXT,
|
||||
"cacheControl" TEXT,
|
||||
"contentDisposition" TEXT,
|
||||
"contentEncoding" TEXT,
|
||||
"customMetadata" TEXT
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "serverpod_cloud_storage_path_idx" ON "serverpod_cloud_storage" ("storageId", "path");
|
||||
CREATE INDEX "serverpod_cloud_storage_expiration" ON "serverpod_cloud_storage" ("expiration");
|
||||
|
||||
|
||||
--
|
||||
-- Class CloudStorageDirectDownloadEntry as table serverpod_cloud_storage_direct_download
|
||||
--
|
||||
CREATE TABLE "serverpod_cloud_storage_direct_download" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"storageId" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"expiration" INTEGER NOT NULL,
|
||||
"authKey" TEXT NOT NULL,
|
||||
"downloadFileName" TEXT,
|
||||
"contentType" TEXT
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "serverpod_cloud_storage_direct_download_auth_key" ON "serverpod_cloud_storage_direct_download" ("authKey");
|
||||
CREATE INDEX "serverpod_cloud_storage_direct_download_expiration" ON "serverpod_cloud_storage_direct_download" ("expiration");
|
||||
|
||||
|
||||
--
|
||||
-- Class CloudStorageDirectUploadEntry as table serverpod_cloud_storage_direct_upload
|
||||
--
|
||||
CREATE TABLE "serverpod_cloud_storage_direct_upload" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"storageId" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"expiration" INTEGER NOT NULL,
|
||||
"authKey" TEXT NOT NULL,
|
||||
"maxFileSize" INTEGER NOT NULL DEFAULT (10485760),
|
||||
"contentLength" INTEGER,
|
||||
"preventOverwrite" INTEGER NOT NULL DEFAULT (0),
|
||||
"contentType" TEXT,
|
||||
"cacheControl" TEXT,
|
||||
"contentDisposition" TEXT,
|
||||
"contentEncoding" TEXT,
|
||||
"customMetadata" TEXT
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "serverpod_cloud_storage_direct_upload_storage_path" ON "serverpod_cloud_storage_direct_upload" ("storageId", "path");
|
||||
|
||||
|
||||
--
|
||||
-- Class FutureCallEntry as table serverpod_future_call
|
||||
--
|
||||
CREATE TABLE "serverpod_future_call" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"time" INTEGER NOT NULL,
|
||||
"serializedObject" TEXT,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"identifier" TEXT,
|
||||
"scheduling" TEXT
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "serverpod_future_call_time_idx" ON "serverpod_future_call" ("time");
|
||||
CREATE INDEX "serverpod_future_call_serverId_idx" ON "serverpod_future_call" ("serverId");
|
||||
CREATE INDEX "serverpod_future_call_identifier_idx" ON "serverpod_future_call" ("identifier");
|
||||
|
||||
|
||||
--
|
||||
-- Class FutureCallClaimEntry as table serverpod_future_call_claim
|
||||
--
|
||||
CREATE TABLE "serverpod_future_call_claim" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"futureCallId" INTEGER,
|
||||
"lastHeartbeatTime" INTEGER NOT NULL,
|
||||
CONSTRAINT "serverpod_future_call_claim_fk_0" FOREIGN KEY ("futureCallId") REFERENCES "serverpod_future_call" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "future_call_unique_idx" ON "serverpod_future_call_claim" ("futureCallId");
|
||||
|
||||
|
||||
--
|
||||
-- Class ServerHealthConnectionInfo as table serverpod_health_connection_info
|
||||
--
|
||||
CREATE TABLE "serverpod_health_connection_info" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"timestamp" INTEGER NOT NULL,
|
||||
"active" INTEGER NOT NULL,
|
||||
"closing" INTEGER NOT NULL,
|
||||
"idle" INTEGER NOT NULL,
|
||||
"granularity" INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "serverpod_health_connection_info_timestamp_idx" ON "serverpod_health_connection_info" ("timestamp", "serverId", "granularity");
|
||||
|
||||
|
||||
--
|
||||
-- Class ServerHealthMetric as table serverpod_health_metric
|
||||
--
|
||||
CREATE TABLE "serverpod_health_metric" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"timestamp" INTEGER NOT NULL,
|
||||
"isHealthy" INTEGER NOT NULL,
|
||||
"value" REAL NOT NULL,
|
||||
"granularity" INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "serverpod_health_metric_timestamp_idx" ON "serverpod_health_metric" ("timestamp", "serverId", "name", "granularity");
|
||||
|
||||
|
||||
--
|
||||
-- Class LogEntry as table serverpod_log
|
||||
--
|
||||
CREATE TABLE "serverpod_log" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"sessionLogId" INTEGER NOT NULL,
|
||||
"messageId" INTEGER,
|
||||
"reference" TEXT,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"time" INTEGER NOT NULL,
|
||||
"logLevel" INTEGER NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"error" TEXT,
|
||||
"stackTrace" TEXT,
|
||||
"order" INTEGER NOT NULL,
|
||||
CONSTRAINT "serverpod_log_fk_0" FOREIGN KEY ("sessionLogId") REFERENCES "serverpod_session_log" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "serverpod_log_sessionLogId_idx" ON "serverpod_log" ("sessionLogId", "order");
|
||||
|
||||
|
||||
--
|
||||
-- Class MessageLogEntry as table serverpod_message_log
|
||||
--
|
||||
CREATE TABLE "serverpod_message_log" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"sessionLogId" INTEGER NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"messageId" INTEGER NOT NULL,
|
||||
"endpoint" TEXT NOT NULL,
|
||||
"messageName" TEXT NOT NULL,
|
||||
"duration" REAL NOT NULL,
|
||||
"error" TEXT,
|
||||
"stackTrace" TEXT,
|
||||
"slow" INTEGER NOT NULL,
|
||||
"order" INTEGER NOT NULL,
|
||||
CONSTRAINT "serverpod_message_log_fk_0" FOREIGN KEY ("sessionLogId") REFERENCES "serverpod_session_log" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "serverpod_message_log_sessionLogId_idx" ON "serverpod_message_log" ("sessionLogId", "order");
|
||||
|
||||
|
||||
--
|
||||
-- Class MethodInfo as table serverpod_method
|
||||
--
|
||||
CREATE TABLE "serverpod_method" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"endpoint" TEXT NOT NULL,
|
||||
"method" TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "serverpod_method_endpoint_method_idx" ON "serverpod_method" ("endpoint", "method");
|
||||
|
||||
|
||||
--
|
||||
-- Class DatabaseMigrationVersion as table serverpod_migrations
|
||||
--
|
||||
CREATE TABLE "serverpod_migrations" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"module" TEXT NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"timestamp" INTEGER
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "serverpod_migrations_ids" ON "serverpod_migrations" ("module");
|
||||
|
||||
|
||||
--
|
||||
-- Class QueryLogEntry as table serverpod_query_log
|
||||
--
|
||||
CREATE TABLE "serverpod_query_log" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"sessionLogId" INTEGER NOT NULL,
|
||||
"messageId" INTEGER,
|
||||
"query" TEXT NOT NULL,
|
||||
"duration" REAL NOT NULL,
|
||||
"numRows" INTEGER,
|
||||
"error" TEXT,
|
||||
"stackTrace" TEXT,
|
||||
"slow" INTEGER NOT NULL,
|
||||
"order" INTEGER NOT NULL,
|
||||
CONSTRAINT "serverpod_query_log_fk_0" FOREIGN KEY ("sessionLogId") REFERENCES "serverpod_session_log" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "serverpod_query_log_sessionLogId_idx" ON "serverpod_query_log" ("sessionLogId", "order");
|
||||
|
||||
|
||||
--
|
||||
-- Class ReadWriteTestEntry as table serverpod_readwrite_test
|
||||
--
|
||||
CREATE TABLE "serverpod_readwrite_test" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"number" INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Class RuntimeSettings as table serverpod_runtime_settings
|
||||
--
|
||||
CREATE TABLE "serverpod_runtime_settings" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"logSettings" TEXT NOT NULL,
|
||||
"logSettingsOverrides" TEXT NOT NULL,
|
||||
"logServiceCalls" INTEGER NOT NULL,
|
||||
"logMalformedCalls" INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Class SessionLogEntry as table serverpod_session_log
|
||||
--
|
||||
CREATE TABLE "serverpod_session_log" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"time" INTEGER NOT NULL,
|
||||
"module" TEXT,
|
||||
"endpoint" TEXT,
|
||||
"method" TEXT,
|
||||
"duration" REAL,
|
||||
"numQueries" INTEGER,
|
||||
"slow" INTEGER,
|
||||
"error" TEXT,
|
||||
"stackTrace" TEXT,
|
||||
"authenticatedUserId" INTEGER,
|
||||
"userId" TEXT,
|
||||
"isOpen" INTEGER,
|
||||
"touched" INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "serverpod_session_log_serverid_idx" ON "serverpod_session_log" ("serverId");
|
||||
CREATE INDEX "serverpod_session_log_time_idx" ON "serverpod_session_log" ("time");
|
||||
CREATE INDEX "serverpod_session_log_touched_idx" ON "serverpod_session_log" ("touched");
|
||||
CREATE INDEX "serverpod_session_log_isopen_idx" ON "serverpod_session_log" ("isOpen");
|
||||
|
||||
|
||||
--
|
||||
-- STORE COLUMN TYPES FOR MIGRATIONS
|
||||
--
|
||||
DROP TABLE IF EXISTS "serverpod_sqlite_schema";
|
||||
|
||||
CREATE TABLE "serverpod_sqlite_schema" (
|
||||
"table_name" TEXT NOT NULL,
|
||||
"column_name" TEXT NOT NULL,
|
||||
"column_type" TEXT NOT NULL,
|
||||
"column_vector_dimension" INTEGER,
|
||||
PRIMARY KEY ("table_name", "column_name")
|
||||
);
|
||||
|
||||
INSERT INTO "serverpod_sqlite_schema" VALUES
|
||||
('crdt_data_attempted_value', 'value', 'jsonb', NULL),
|
||||
('crdt_data_fields', 'hlcDatetime', 'timestampWithoutTimeZone', NULL),
|
||||
('crdt_data_rows', 'hlcDatetime', 'timestampWithoutTimeZone', NULL),
|
||||
('crdt_data_rows', 'uuidRowId', 'uuid', NULL),
|
||||
('crdt_data_tombstone', 'hlcDatetime', 'timestampWithoutTimeZone', NULL),
|
||||
('crdt_nodes', 'uuidNodeId', 'uuid', NULL),
|
||||
('crdt_nodes', 'lastHlc', 'jsonb', NULL),
|
||||
('crdt_schema_columns', 'isNullable', 'boolean', NULL),
|
||||
('offline_sync_integrity_violations', 'uuidRowId', 'uuid', NULL),
|
||||
('offline_sync_integrity_violations', 'ownerSpaceUuid', 'uuid', NULL),
|
||||
('offline_sync_integrity_violations', 'incomingSpaceUuid', 'uuid', NULL),
|
||||
('offline_sync_integrity_violations', 'uuidNodeId', 'uuid', NULL),
|
||||
('offline_sync_integrity_violations', 'hlcDatetime', 'timestampWithoutTimeZone', NULL),
|
||||
('offline_sync_integrity_violations', 'firstSeenAt', 'timestampWithoutTimeZone', NULL),
|
||||
('offline_sync_integrity_violations', 'lastSeenAt', 'timestampWithoutTimeZone', NULL),
|
||||
('offline_sync_space_members', 'userUuid', 'uuid', NULL),
|
||||
('offline_sync_space_nodes', 'lastReceivedHlc', 'jsonb', NULL),
|
||||
('offline_sync_spaces', 'uuidSpaceId', 'uuid', NULL),
|
||||
('serverpod_cloud_storage', 'addedTime', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_cloud_storage', 'expiration', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_cloud_storage', 'verified', 'boolean', NULL),
|
||||
('serverpod_cloud_storage_direct_download', 'expiration', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_cloud_storage_direct_upload', 'expiration', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_cloud_storage_direct_upload', 'preventOverwrite', 'boolean', NULL),
|
||||
('serverpod_future_call', 'time', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_future_call', 'scheduling', 'json', NULL),
|
||||
('serverpod_future_call_claim', 'lastHeartbeatTime', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_health_connection_info', 'timestamp', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_health_metric', 'timestamp', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_health_metric', 'isHealthy', 'boolean', NULL),
|
||||
('serverpod_log', 'time', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_message_log', 'slow', 'boolean', NULL),
|
||||
('serverpod_migrations', 'timestamp', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_query_log', 'slow', 'boolean', NULL),
|
||||
('serverpod_runtime_settings', 'logSettings', 'json', NULL),
|
||||
('serverpod_runtime_settings', 'logSettingsOverrides', 'json', NULL),
|
||||
('serverpod_runtime_settings', 'logServiceCalls', 'boolean', NULL),
|
||||
('serverpod_runtime_settings', 'logMalformedCalls', 'boolean', NULL),
|
||||
('serverpod_session_log', 'time', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_session_log', 'slow', 'boolean', NULL),
|
||||
('serverpod_session_log', 'isOpen', 'boolean', NULL),
|
||||
('serverpod_session_log', 'touched', 'timestampWithoutTimeZone', NULL);
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_offline_sync
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_offline_sync', '20260914143806119', (unixepoch('now', 'subsecond') * 1000))
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260914143806119', "timestamp" = (unixepoch('now', 'subsecond') * 1000);
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod', '20260824182259319', (unixepoch('now', 'subsecond') * 1000))
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260824182259319', "timestamp" = (unixepoch('now', 'subsecond') * 1000);
|
||||
|
||||
|
||||
COMMIT;
|
||||
1073
packages/serverpod_offline_sync_server/migrations/20260914143806119/definition_project.json
generated
Normal file
1073
packages/serverpod_offline_sync_server/migrations/20260914143806119/definition_project.json
generated
Normal file
File diff suppressed because it is too large
Load diff
670
packages/serverpod_offline_sync_server/migrations/20260914143806119/migration.json
generated
Normal file
670
packages/serverpod_offline_sync_server/migrations/20260914143806119/migration.json
generated
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "deleteTable",
|
||||
"deleteTable": "crdt_sync_integrity_violations"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "deleteTable",
|
||||
"deleteTable": "crdt_scopes"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "deleteTable",
|
||||
"deleteTable": "crdt_scope_nodes"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "deleteTable",
|
||||
"deleteTable": "crdt_scope_members"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "deleteTable",
|
||||
"deleteTable": "crdt_data_rows"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "crdt_data_rows",
|
||||
"dartName": "CrdtDataRow",
|
||||
"module": "serverpod_offline_sync",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"columnDefault": "serial",
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "hlcDatetime",
|
||||
"fieldName": "hlcDatetime",
|
||||
"columnType": 4,
|
||||
"isNullable": false,
|
||||
"dartType": "DateTime"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "hlcCounter",
|
||||
"fieldName": "hlcCounter",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "tblId",
|
||||
"fieldName": "tblId",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "uuidRowId",
|
||||
"fieldName": "uuidRowId",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"dartType": "UuidValue"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nodeId",
|
||||
"fieldName": "nodeId",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "visibility",
|
||||
"fieldName": "visibility",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"columnDefault": "0",
|
||||
"dartType": "serverpod_offline_sync:CrdtDataRowVisibility"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "crdt_data_rows_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "crdt_data_rows_fk_1",
|
||||
"columns": [
|
||||
"tblId"
|
||||
],
|
||||
"referenceTable": "crdt_schema_tables",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "crdt_data_rows_fk_2",
|
||||
"columns": [
|
||||
"nodeId"
|
||||
],
|
||||
"referenceTable": "crdt_nodes",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 3
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "crdt_data_rows_space_tbl_row_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "tblId"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "uuidRowId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": true,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "offline_sync_integrity_violations",
|
||||
"dartName": "OfflineSyncIntegrityViolation",
|
||||
"module": "serverpod_offline_sync",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"columnDefault": "serial",
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "type",
|
||||
"fieldName": "type",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "serverpod_offline_sync:OfflineSyncViolationType"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "domainTableName",
|
||||
"fieldName": "domainTableName",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "uuidRowId",
|
||||
"fieldName": "uuidRowId",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"dartType": "UuidValue"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "ownerSpaceUuid",
|
||||
"fieldName": "ownerSpaceUuid",
|
||||
"columnType": 7,
|
||||
"isNullable": true,
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "incomingSpaceUuid",
|
||||
"fieldName": "incomingSpaceUuid",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"dartType": "UuidValue"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "operation",
|
||||
"fieldName": "operation",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "serverpod_offline_sync:OfflineSyncViolationOperation"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "uuidNodeId",
|
||||
"fieldName": "uuidNodeId",
|
||||
"columnType": 7,
|
||||
"isNullable": true,
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "crdtDataRowId",
|
||||
"fieldName": "crdtDataRowId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "hlcDatetime",
|
||||
"fieldName": "hlcDatetime",
|
||||
"columnType": 4,
|
||||
"isNullable": true,
|
||||
"dartType": "DateTime?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "hlcCounter",
|
||||
"fieldName": "hlcCounter",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "firstSeenAt",
|
||||
"fieldName": "firstSeenAt",
|
||||
"columnType": 4,
|
||||
"isNullable": false,
|
||||
"dartType": "DateTime"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "lastSeenAt",
|
||||
"fieldName": "lastSeenAt",
|
||||
"columnType": 4,
|
||||
"isNullable": false,
|
||||
"dartType": "DateTime"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "occurrences",
|
||||
"fieldName": "occurrences",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "offline_sync_integrity_violations_key_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "type"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "operation"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "domainTableName"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "uuidRowId"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "ownerSpaceUuid"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "incomingSpaceUuid"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": true,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "offline_sync_space_members",
|
||||
"dartName": "OfflineSyncSpaceMember",
|
||||
"module": "serverpod_offline_sync",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"columnDefault": "serial",
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "userUuid",
|
||||
"fieldName": "userUuid",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"dartType": "UuidValue"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "role",
|
||||
"fieldName": "role",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "serverpod_offline_sync:OfflineSyncSpaceRole"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "offline_sync_space_members_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "offline_sync_space_member_unique_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "userUuid"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": true,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "offline_sync_space_nodes",
|
||||
"dartName": "OfflineSyncSpaceNode",
|
||||
"module": "serverpod_offline_sync",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"columnDefault": "serial",
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nodeId",
|
||||
"fieldName": "nodeId",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "lastReceivedHlc",
|
||||
"fieldName": "lastReceivedHlc",
|
||||
"columnType": 14,
|
||||
"isNullable": true,
|
||||
"dartType": "package:serverpod_offline_sync/serverpod_offline_sync.dart:Hlc?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "offline_sync_space_nodes_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "offline_sync_space_nodes_fk_1",
|
||||
"columns": [
|
||||
"nodeId"
|
||||
],
|
||||
"referenceTable": "crdt_nodes",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "offline_sync_space_node_unique_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "nodeId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": true,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "offline_sync_spaces",
|
||||
"dartName": "OfflineSyncSpace",
|
||||
"module": "serverpod_offline_sync",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"columnDefault": "serial",
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "uuidSpaceId",
|
||||
"fieldName": "uuidSpaceId",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "currentNodeId",
|
||||
"fieldName": "currentNodeId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "offline_sync_spaces_fk_0",
|
||||
"columns": [
|
||||
"currentNodeId"
|
||||
],
|
||||
"referenceTable": "crdt_nodes",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 3
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "offline_sync_spaces__uuidSpaceId__unique_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "uuidSpaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": true,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationWarning",
|
||||
"type": "tableDropped",
|
||||
"message": "Table \"crdt_sync_integrity_violations\" will be dropped.",
|
||||
"table": "crdt_sync_integrity_violations",
|
||||
"columns": [],
|
||||
"destrucive": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationWarning",
|
||||
"type": "tableDropped",
|
||||
"message": "Table \"crdt_scopes\" will be dropped.",
|
||||
"table": "crdt_scopes",
|
||||
"columns": [],
|
||||
"destrucive": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationWarning",
|
||||
"type": "tableDropped",
|
||||
"message": "Table \"crdt_scope_nodes\" will be dropped.",
|
||||
"table": "crdt_scope_nodes",
|
||||
"columns": [],
|
||||
"destrucive": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationWarning",
|
||||
"type": "tableDropped",
|
||||
"message": "Table \"crdt_scope_members\" will be dropped.",
|
||||
"table": "crdt_scope_members",
|
||||
"columns": [],
|
||||
"destrucive": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationWarning",
|
||||
"type": "columnDropped",
|
||||
"message": "Column \"scopeId\" of table \"crdt_data_rows\" will be dropped.",
|
||||
"table": "crdt_data_rows",
|
||||
"columns": [
|
||||
"scopeId"
|
||||
],
|
||||
"destrucive": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationWarning",
|
||||
"type": "uniqueIndexCreated",
|
||||
"message": "Unique index \"crdt_data_rows_space_tbl_row_idx\" is added to table \"crdt_data_rows\". If there are existing rows with duplicate values, this migration will fail.",
|
||||
"table": "crdt_data_rows",
|
||||
"columns": [
|
||||
"spaceId",
|
||||
"tblId",
|
||||
"uuidRowId"
|
||||
],
|
||||
"destrucive": false
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationWarning",
|
||||
"type": "tableDropped",
|
||||
"message": "One or more columns are added to table \"crdt_data_rows\" which cannot be added in a table migration. The complete table will be deleted and recreated.",
|
||||
"table": "crdt_data_rows",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"destrucive": true
|
||||
}
|
||||
],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
187
packages/serverpod_offline_sync_server/migrations/20260914143806119/migration.sql
generated
Normal file
187
packages/serverpod_offline_sync_server/migrations/20260914143806119/migration.sql
generated
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
BEGIN;
|
||||
|
||||
--
|
||||
-- ACTION DROP TABLE
|
||||
--
|
||||
DROP TABLE "crdt_sync_integrity_violations";
|
||||
|
||||
--
|
||||
-- ACTION DROP TABLE
|
||||
--
|
||||
DROP TABLE "crdt_scopes";
|
||||
|
||||
--
|
||||
-- ACTION DROP TABLE
|
||||
--
|
||||
DROP TABLE "crdt_scope_nodes";
|
||||
|
||||
--
|
||||
-- ACTION DROP TABLE
|
||||
--
|
||||
DROP TABLE "crdt_scope_members";
|
||||
|
||||
--
|
||||
-- ACTION DROP TABLE
|
||||
--
|
||||
DROP TABLE "crdt_data_rows";
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "crdt_data_rows" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"hlcDatetime" INTEGER NOT NULL,
|
||||
"hlcCounter" INTEGER NOT NULL,
|
||||
"spaceId" INTEGER NOT NULL,
|
||||
"tblId" INTEGER NOT NULL,
|
||||
"uuidRowId" BLOB NOT NULL,
|
||||
"nodeId" INTEGER NOT NULL,
|
||||
"visibility" INTEGER NOT NULL DEFAULT (0),
|
||||
CONSTRAINT "crdt_data_rows_fk_0" FOREIGN KEY ("spaceId") REFERENCES "offline_sync_spaces" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "crdt_data_rows_fk_1" FOREIGN KEY ("tblId") REFERENCES "crdt_schema_tables" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "crdt_data_rows_fk_2" FOREIGN KEY ("nodeId") REFERENCES "crdt_nodes" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "crdt_data_rows_space_tbl_row_idx" ON "crdt_data_rows" ("spaceId", "tblId", "uuidRowId");
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "offline_sync_integrity_violations" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"type" TEXT NOT NULL,
|
||||
"domainTableName" TEXT NOT NULL,
|
||||
"uuidRowId" BLOB NOT NULL,
|
||||
"ownerSpaceUuid" BLOB,
|
||||
"incomingSpaceUuid" BLOB NOT NULL,
|
||||
"operation" TEXT NOT NULL,
|
||||
"uuidNodeId" BLOB,
|
||||
"crdtDataRowId" INTEGER,
|
||||
"hlcDatetime" INTEGER,
|
||||
"hlcCounter" INTEGER,
|
||||
"firstSeenAt" INTEGER NOT NULL,
|
||||
"lastSeenAt" INTEGER NOT NULL,
|
||||
"occurrences" INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "offline_sync_integrity_violations_key_idx" ON "offline_sync_integrity_violations" ("type", "operation", "domainTableName", "uuidRowId", "ownerSpaceUuid", "incomingSpaceUuid");
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "offline_sync_space_members" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"spaceId" INTEGER NOT NULL,
|
||||
"userUuid" BLOB NOT NULL,
|
||||
"role" TEXT NOT NULL,
|
||||
CONSTRAINT "offline_sync_space_members_fk_0" FOREIGN KEY ("spaceId") REFERENCES "offline_sync_spaces" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "offline_sync_space_member_unique_idx" ON "offline_sync_space_members" ("userUuid", "spaceId");
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "offline_sync_space_nodes" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"spaceId" INTEGER NOT NULL,
|
||||
"nodeId" INTEGER NOT NULL,
|
||||
"lastReceivedHlc" BLOB,
|
||||
CONSTRAINT "offline_sync_space_nodes_fk_0" FOREIGN KEY ("spaceId") REFERENCES "offline_sync_spaces" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "offline_sync_space_nodes_fk_1" FOREIGN KEY ("nodeId") REFERENCES "crdt_nodes" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "offline_sync_space_node_unique_idx" ON "offline_sync_space_nodes" ("spaceId", "nodeId");
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "offline_sync_spaces" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"uuidSpaceId" BLOB NOT NULL DEFAULT (unhex(printf('%012x', CAST(unixepoch('now', 'subsecond') * 1000 AS INTEGER)) || '7' || substr(hex(randomblob(2)), 2, 3) || substr('89AB', 1 + (abs(random()) % 4), 1) || substr(hex(randomblob(8)), 2, 15))),
|
||||
"currentNodeId" INTEGER,
|
||||
CONSTRAINT "offline_sync_spaces_fk_0" FOREIGN KEY ("currentNodeId") REFERENCES "crdt_nodes" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
) STRICT;
|
||||
|
||||
-- Indexes
|
||||
CREATE UNIQUE INDEX "offline_sync_spaces__uuidSpaceId__unique_idx" ON "offline_sync_spaces" ("uuidSpaceId");
|
||||
|
||||
--
|
||||
-- STORE COLUMN TYPES FOR MIGRATIONS
|
||||
--
|
||||
DROP TABLE IF EXISTS "serverpod_sqlite_schema";
|
||||
|
||||
CREATE TABLE "serverpod_sqlite_schema" (
|
||||
"table_name" TEXT NOT NULL,
|
||||
"column_name" TEXT NOT NULL,
|
||||
"column_type" TEXT NOT NULL,
|
||||
"column_vector_dimension" INTEGER,
|
||||
PRIMARY KEY ("table_name", "column_name")
|
||||
);
|
||||
|
||||
INSERT INTO "serverpod_sqlite_schema" VALUES
|
||||
('crdt_data_attempted_value', 'value', 'jsonb', NULL),
|
||||
('crdt_data_fields', 'hlcDatetime', 'timestampWithoutTimeZone', NULL),
|
||||
('crdt_data_rows', 'hlcDatetime', 'timestampWithoutTimeZone', NULL),
|
||||
('crdt_data_rows', 'uuidRowId', 'uuid', NULL),
|
||||
('crdt_data_tombstone', 'hlcDatetime', 'timestampWithoutTimeZone', NULL),
|
||||
('crdt_nodes', 'uuidNodeId', 'uuid', NULL),
|
||||
('crdt_nodes', 'lastHlc', 'jsonb', NULL),
|
||||
('crdt_schema_columns', 'isNullable', 'boolean', NULL),
|
||||
('offline_sync_integrity_violations', 'uuidRowId', 'uuid', NULL),
|
||||
('offline_sync_integrity_violations', 'ownerSpaceUuid', 'uuid', NULL),
|
||||
('offline_sync_integrity_violations', 'incomingSpaceUuid', 'uuid', NULL),
|
||||
('offline_sync_integrity_violations', 'uuidNodeId', 'uuid', NULL),
|
||||
('offline_sync_integrity_violations', 'hlcDatetime', 'timestampWithoutTimeZone', NULL),
|
||||
('offline_sync_integrity_violations', 'firstSeenAt', 'timestampWithoutTimeZone', NULL),
|
||||
('offline_sync_integrity_violations', 'lastSeenAt', 'timestampWithoutTimeZone', NULL),
|
||||
('offline_sync_space_members', 'userUuid', 'uuid', NULL),
|
||||
('offline_sync_space_nodes', 'lastReceivedHlc', 'jsonb', NULL),
|
||||
('offline_sync_spaces', 'uuidSpaceId', 'uuid', NULL),
|
||||
('serverpod_cloud_storage', 'addedTime', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_cloud_storage', 'expiration', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_cloud_storage', 'verified', 'boolean', NULL),
|
||||
('serverpod_cloud_storage_direct_download', 'expiration', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_cloud_storage_direct_upload', 'expiration', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_cloud_storage_direct_upload', 'preventOverwrite', 'boolean', NULL),
|
||||
('serverpod_future_call', 'time', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_future_call', 'scheduling', 'json', NULL),
|
||||
('serverpod_future_call_claim', 'lastHeartbeatTime', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_health_connection_info', 'timestamp', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_health_metric', 'timestamp', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_health_metric', 'isHealthy', 'boolean', NULL),
|
||||
('serverpod_log', 'time', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_message_log', 'slow', 'boolean', NULL),
|
||||
('serverpod_migrations', 'timestamp', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_query_log', 'slow', 'boolean', NULL),
|
||||
('serverpod_runtime_settings', 'logSettings', 'json', NULL),
|
||||
('serverpod_runtime_settings', 'logSettingsOverrides', 'json', NULL),
|
||||
('serverpod_runtime_settings', 'logServiceCalls', 'boolean', NULL),
|
||||
('serverpod_runtime_settings', 'logMalformedCalls', 'boolean', NULL),
|
||||
('serverpod_session_log', 'time', 'timestampWithoutTimeZone', NULL),
|
||||
('serverpod_session_log', 'slow', 'boolean', NULL),
|
||||
('serverpod_session_log', 'isOpen', 'boolean', NULL),
|
||||
('serverpod_session_log', 'touched', 'timestampWithoutTimeZone', NULL);
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_offline_sync
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_offline_sync', '20260914143806119', (unixepoch('now', 'subsecond') * 1000))
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260914143806119', "timestamp" = (unixepoch('now', 'subsecond') * 1000);
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod', '20260824182259319', (unixepoch('now', 'subsecond') * 1000))
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260824182259319', "timestamp" = (unixepoch('now', 'subsecond') * 1000);
|
||||
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -8,3 +8,4 @@
|
|||
20260831202217145
|
||||
20260902184216091
|
||||
20260902195732182
|
||||
20260914143806119
|
||||
|
|
|
|||
|
|
@ -1,358 +0,0 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('[CRDT scopes]', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
|
||||
setUp(() {
|
||||
session = sessionBuilder.build();
|
||||
session.serverpod.initializeCrdtSync(syncTables: []);
|
||||
});
|
||||
|
||||
group('Given a database with no shared scopes,', () {
|
||||
test(
|
||||
'when createFor is called with only a user, '
|
||||
'then the user is granted readWrite access to the new scope.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final scope = await session.crdt.scopes.createFor(user);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await CrdtScopeMembership.memberScopes(session, user),
|
||||
contains(scope),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when createFor is called with a user and a role, '
|
||||
'then the user is granted that role.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final scope = await session.crdt.scopes.createFor(
|
||||
user,
|
||||
role: CrdtScopeRole.readOnly,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.members(scope),
|
||||
{user: CrdtScopeRole.readOnly},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when create is called without grants, '
|
||||
'then the new shared scope is dormant and has no explicit members.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final scope = await session.crdt.scopes.create();
|
||||
|
||||
expect(await session.crdt.scopes.members(scope), isEmpty);
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await CrdtScopeMembership.memberScopes(session, user),
|
||||
isNot(contains(scope)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when create is called with several grants, '
|
||||
'then every membership is stored with its role.',
|
||||
() async {
|
||||
final writer = const Uuid().v7obj();
|
||||
final reader = const Uuid().v7obj();
|
||||
|
||||
final scope = await session.crdt.scopes.create(
|
||||
grants: {
|
||||
writer: CrdtScopeRole.readWrite,
|
||||
reader: CrdtScopeRole.readOnly,
|
||||
},
|
||||
);
|
||||
|
||||
expect(await session.crdt.scopes.members(scope), {
|
||||
writer: CrdtScopeRole.readWrite,
|
||||
reader: CrdtScopeRole.readOnly,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when roleOf is called for a personal scope with no row, '
|
||||
'then readWrite is returned.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: user),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
expect(await session.crdt.scopes.members(user), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when createFor is called inside a transaction that rolls back, '
|
||||
'then no scope or membership rows leak.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
final countsBefore = await _membershipRowCounts(session);
|
||||
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await session.crdt.scopes.createFor(user, transaction: tx);
|
||||
throw StateError('rollback');
|
||||
}),
|
||||
throwsStateError,
|
||||
);
|
||||
|
||||
expect(await _membershipRowCounts(session), countsBefore);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grant is called for an unknown shared scope, '
|
||||
'then CrdtScopeNotFoundException is thrown.',
|
||||
() async {
|
||||
final missingScope = const Uuid().v7obj();
|
||||
|
||||
await expectLater(
|
||||
session.crdt.scopes.grant(
|
||||
scope: missingScope,
|
||||
user: const Uuid().v7obj(),
|
||||
role: CrdtScopeRole.readWrite,
|
||||
),
|
||||
throwsA(
|
||||
isA<CrdtScopeNotFoundException>().having(
|
||||
(error) => error.scope,
|
||||
'scope',
|
||||
missingScope,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a dormant shared scope,', () {
|
||||
late UuidValue scope;
|
||||
|
||||
setUp(() async {
|
||||
scope = await session.crdt.scopes.create();
|
||||
});
|
||||
|
||||
test(
|
||||
'when grant is called, '
|
||||
'then the membership is inserted.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
await session.crdt.scopes.grant(
|
||||
scope: scope,
|
||||
user: user,
|
||||
role: CrdtScopeRole.readOnly,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
CrdtScopeRole.readOnly,
|
||||
);
|
||||
expect(
|
||||
await session.crdt.scopes.members(scope),
|
||||
{user: CrdtScopeRole.readOnly},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grantAll is called with several users, '
|
||||
'then all memberships are applied.',
|
||||
() async {
|
||||
final readOnlyUser = const Uuid().v7obj();
|
||||
final readWriteUser = const Uuid().v7obj();
|
||||
|
||||
await session.crdt.scopes.grantAll(scope, {
|
||||
readOnlyUser: CrdtScopeRole.readOnly,
|
||||
readWriteUser: CrdtScopeRole.readWrite,
|
||||
});
|
||||
|
||||
expect(await session.crdt.scopes.members(scope), {
|
||||
readOnlyUser: CrdtScopeRole.readOnly,
|
||||
readWriteUser: CrdtScopeRole.readWrite,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grant is called inside a transaction that rolls back, '
|
||||
'then no membership row leaks.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
final countsBefore = await _membershipRowCounts(session);
|
||||
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await session.crdt.scopes.grant(
|
||||
scope: scope,
|
||||
user: user,
|
||||
role: CrdtScopeRole.readWrite,
|
||||
transaction: tx,
|
||||
);
|
||||
throw StateError('rollback');
|
||||
}),
|
||||
throwsStateError,
|
||||
);
|
||||
|
||||
expect(await _membershipRowCounts(session), countsBefore);
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a readOnly member on a shared scope,', () {
|
||||
late UuidValue user;
|
||||
late UuidValue scope;
|
||||
|
||||
setUp(() async {
|
||||
user = const Uuid().v7obj();
|
||||
scope = await session.crdt.scopes.create(
|
||||
grants: {user: CrdtScopeRole.readOnly},
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when grant is called again for the same member with readWrite, '
|
||||
'then the existing membership is updated.',
|
||||
() async {
|
||||
await session.crdt.scopes.grant(
|
||||
scope: scope,
|
||||
user: user,
|
||||
role: CrdtScopeRole.readWrite,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await session.crdt.scopes.members(scope),
|
||||
{user: CrdtScopeRole.readWrite},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a shared scope that has one readWrite member,', () {
|
||||
late UuidValue member;
|
||||
late UuidValue nonMember;
|
||||
late UuidValue scope;
|
||||
|
||||
setUp(() async {
|
||||
member = const Uuid().v7obj();
|
||||
nonMember = const Uuid().v7obj();
|
||||
scope = await session.crdt.scopes.createFor(member);
|
||||
});
|
||||
|
||||
test(
|
||||
'when revoke is called for the member, '
|
||||
'then the membership is removed.',
|
||||
() async {
|
||||
await session.crdt.scopes.revoke(scope: scope, user: member);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: member, scope: scope),
|
||||
isNull,
|
||||
);
|
||||
expect(await session.crdt.scopes.members(scope), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when revoke is called for a non-member, '
|
||||
'then it completes without changing the scope members.',
|
||||
() async {
|
||||
await expectLater(
|
||||
session.crdt.scopes.revoke(scope: scope, user: nonMember),
|
||||
completes,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.members(scope),
|
||||
{member: CrdtScopeRole.readWrite},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when roleOf is called for stored and missing memberships, '
|
||||
'then it returns the stored role or null.',
|
||||
() async {
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: member, scope: scope),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: nonMember, scope: scope),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with populated and dormant shared scopes,', () {
|
||||
late UuidValue readOnlyUser;
|
||||
late UuidValue readWriteUser;
|
||||
late UuidValue populatedScope;
|
||||
late UuidValue dormantScope;
|
||||
|
||||
setUp(() async {
|
||||
readOnlyUser = const Uuid().v7obj();
|
||||
readWriteUser = const Uuid().v7obj();
|
||||
populatedScope = await session.crdt.scopes.create(
|
||||
grants: {
|
||||
readOnlyUser: CrdtScopeRole.readOnly,
|
||||
readWriteUser: CrdtScopeRole.readWrite,
|
||||
},
|
||||
);
|
||||
dormantScope = await session.crdt.scopes.create();
|
||||
});
|
||||
|
||||
test(
|
||||
'when members is called, '
|
||||
'then explicit membership maps are returned.',
|
||||
() async {
|
||||
expect(await session.crdt.scopes.members(populatedScope), {
|
||||
readOnlyUser: CrdtScopeRole.readOnly,
|
||||
readWriteUser: CrdtScopeRole.readWrite,
|
||||
});
|
||||
expect(await session.crdt.scopes.members(dormantScope), isEmpty);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, int>> _membershipRowCounts(Session session) async {
|
||||
return {
|
||||
CrdtScope.t.tableName: await CrdtScope.db.count(session),
|
||||
CrdtScopeMember.t.tableName: await CrdtScopeMember.db.count(session),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('[CRDT spaces]', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
|
||||
setUp(() {
|
||||
session = sessionBuilder.build();
|
||||
session.serverpod.initializeOfflineSync(syncTables: []);
|
||||
});
|
||||
|
||||
group('Given a database with no shared spaces,', () {
|
||||
test(
|
||||
'when createFor is called with only a user, '
|
||||
'then the user is granted readWrite access to the new space.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final space = await session.offlineSync.spaces.createFor(user);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.memberSpaces(session, user),
|
||||
contains(space),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when createFor is called with a user and a role, '
|
||||
'then the user is granted that role.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final space = await session.offlineSync.spaces.createFor(
|
||||
user,
|
||||
role: OfflineSyncSpaceRole.readOnly,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.members(space),
|
||||
{user: OfflineSyncSpaceRole.readOnly},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when create is called without grants, '
|
||||
'then the new shared space is dormant and has no explicit members.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final space = await session.offlineSync.spaces.create();
|
||||
|
||||
expect(await session.offlineSync.spaces.members(space), isEmpty);
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.memberSpaces(session, user),
|
||||
isNot(contains(space)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when create is called with several grants, '
|
||||
'then every membership is stored with its role.',
|
||||
() async {
|
||||
final writer = const Uuid().v7obj();
|
||||
final reader = const Uuid().v7obj();
|
||||
|
||||
final space = await session.offlineSync.spaces.create(
|
||||
grants: {
|
||||
writer: OfflineSyncSpaceRole.readWrite,
|
||||
reader: OfflineSyncSpaceRole.readOnly,
|
||||
},
|
||||
);
|
||||
|
||||
expect(await session.offlineSync.spaces.members(space), {
|
||||
writer: OfflineSyncSpaceRole.readWrite,
|
||||
reader: OfflineSyncSpaceRole.readOnly,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when roleOf is called for a personal space with no row, '
|
||||
'then readWrite is returned.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: user),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
expect(await session.offlineSync.spaces.members(user), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when createFor is called inside a transaction that rolls back, '
|
||||
'then no space or membership rows leak.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
final countsBefore = await _membershipRowCounts(session);
|
||||
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await session.offlineSync.spaces.createFor(user, transaction: tx);
|
||||
throw StateError('rollback');
|
||||
}),
|
||||
throwsStateError,
|
||||
);
|
||||
|
||||
expect(await _membershipRowCounts(session), countsBefore);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grant is called for an unknown shared space, '
|
||||
'then OfflineSyncSpaceNotFoundException is thrown.',
|
||||
() async {
|
||||
final missingSpace = const Uuid().v7obj();
|
||||
|
||||
await expectLater(
|
||||
session.offlineSync.spaces.grant(
|
||||
space: missingSpace,
|
||||
user: const Uuid().v7obj(),
|
||||
role: OfflineSyncSpaceRole.readWrite,
|
||||
),
|
||||
throwsA(
|
||||
isA<OfflineSyncSpaceNotFoundException>().having(
|
||||
(error) => error.space,
|
||||
'space',
|
||||
missingSpace,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a dormant shared space,', () {
|
||||
late UuidValue space;
|
||||
|
||||
setUp(() async {
|
||||
space = await session.offlineSync.spaces.create();
|
||||
});
|
||||
|
||||
test(
|
||||
'when grant is called, '
|
||||
'then the membership is inserted.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
await session.offlineSync.spaces.grant(
|
||||
space: space,
|
||||
user: user,
|
||||
role: OfflineSyncSpaceRole.readOnly,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
OfflineSyncSpaceRole.readOnly,
|
||||
);
|
||||
expect(
|
||||
await session.offlineSync.spaces.members(space),
|
||||
{user: OfflineSyncSpaceRole.readOnly},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grantAll is called with several users, '
|
||||
'then all memberships are applied.',
|
||||
() async {
|
||||
final readOnlyUser = const Uuid().v7obj();
|
||||
final readWriteUser = const Uuid().v7obj();
|
||||
|
||||
await session.offlineSync.spaces.grantAll(space, {
|
||||
readOnlyUser: OfflineSyncSpaceRole.readOnly,
|
||||
readWriteUser: OfflineSyncSpaceRole.readWrite,
|
||||
});
|
||||
|
||||
expect(await session.offlineSync.spaces.members(space), {
|
||||
readOnlyUser: OfflineSyncSpaceRole.readOnly,
|
||||
readWriteUser: OfflineSyncSpaceRole.readWrite,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grant is called inside a transaction that rolls back, '
|
||||
'then no membership row leaks.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
final countsBefore = await _membershipRowCounts(session);
|
||||
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await session.offlineSync.spaces.grant(
|
||||
space: space,
|
||||
user: user,
|
||||
role: OfflineSyncSpaceRole.readWrite,
|
||||
transaction: tx,
|
||||
);
|
||||
throw StateError('rollback');
|
||||
}),
|
||||
throwsStateError,
|
||||
);
|
||||
|
||||
expect(await _membershipRowCounts(session), countsBefore);
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a readOnly member on a shared space,', () {
|
||||
late UuidValue user;
|
||||
late UuidValue space;
|
||||
|
||||
setUp(() async {
|
||||
user = const Uuid().v7obj();
|
||||
space = await session.offlineSync.spaces.create(
|
||||
grants: {user: OfflineSyncSpaceRole.readOnly},
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when grant is called again for the same member with readWrite, '
|
||||
'then the existing membership is updated.',
|
||||
() async {
|
||||
await session.offlineSync.spaces.grant(
|
||||
space: space,
|
||||
user: user,
|
||||
role: OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await session.offlineSync.spaces.members(space),
|
||||
{user: OfflineSyncSpaceRole.readWrite},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a shared space that has one readWrite member,', () {
|
||||
late UuidValue member;
|
||||
late UuidValue nonMember;
|
||||
late UuidValue space;
|
||||
|
||||
setUp(() async {
|
||||
member = const Uuid().v7obj();
|
||||
nonMember = const Uuid().v7obj();
|
||||
space = await session.offlineSync.spaces.createFor(member);
|
||||
});
|
||||
|
||||
test(
|
||||
'when revoke is called for the member, '
|
||||
'then the membership is removed.',
|
||||
() async {
|
||||
await session.offlineSync.spaces.revoke(space: space, user: member);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: member, space: space),
|
||||
isNull,
|
||||
);
|
||||
expect(await session.offlineSync.spaces.members(space), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when revoke is called for a non-member, '
|
||||
'then it completes without changing the space members.',
|
||||
() async {
|
||||
await expectLater(
|
||||
session.offlineSync.spaces.revoke(space: space, user: nonMember),
|
||||
completes,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.members(space),
|
||||
{member: OfflineSyncSpaceRole.readWrite},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when roleOf is called for stored and missing memberships, '
|
||||
'then it returns the stored role or null.',
|
||||
() async {
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: member, space: space),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: nonMember, space: space),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with populated and dormant shared spaces,', () {
|
||||
late UuidValue readOnlyUser;
|
||||
late UuidValue readWriteUser;
|
||||
late UuidValue populatedSpace;
|
||||
late UuidValue dormantSpace;
|
||||
|
||||
setUp(() async {
|
||||
readOnlyUser = const Uuid().v7obj();
|
||||
readWriteUser = const Uuid().v7obj();
|
||||
populatedSpace = await session.offlineSync.spaces.create(
|
||||
grants: {
|
||||
readOnlyUser: OfflineSyncSpaceRole.readOnly,
|
||||
readWriteUser: OfflineSyncSpaceRole.readWrite,
|
||||
},
|
||||
);
|
||||
dormantSpace = await session.offlineSync.spaces.create();
|
||||
});
|
||||
|
||||
test(
|
||||
'when members is called, '
|
||||
'then explicit membership maps are returned.',
|
||||
() async {
|
||||
expect(await session.offlineSync.spaces.members(populatedSpace), {
|
||||
readOnlyUser: OfflineSyncSpaceRole.readOnly,
|
||||
readWriteUser: OfflineSyncSpaceRole.readWrite,
|
||||
});
|
||||
expect(await session.offlineSync.spaces.members(dormantSpace), isEmpty);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, int>> _membershipRowCounts(Session session) async {
|
||||
return {
|
||||
OfflineSyncSpace.t.tableName: await OfflineSyncSpace.db.count(session),
|
||||
OfflineSyncSpaceMember.t.tableName: await OfflineSyncSpaceMember.db.count(session),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:serverpod_offline_sync_server/src/generated/protocol.dart' as server;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('[CRDT Scope Membership]', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
|
||||
setUp(() {
|
||||
session = sessionBuilder.build();
|
||||
});
|
||||
|
||||
group(
|
||||
'Given a user with personal scope access and readWrite membership in one shared CRDT scope,',
|
||||
() {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedScopeUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedScopeUuid = const Uuid().v7obj();
|
||||
|
||||
final sharedScope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: sharedScopeUuid),
|
||||
);
|
||||
await server.CrdtScopeMember.db.insertRow(
|
||||
session,
|
||||
server.CrdtScopeMember(
|
||||
scopeId: sharedScope.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.CrdtScopeRole.readWrite,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when member scopes are resolved, '
|
||||
'then only the personal scope and the granted shared scope are included.',
|
||||
() async {
|
||||
final scopes = await CrdtScopeMembership.memberScopes(session, userUuid);
|
||||
final expectedScopes = [userUuid, sharedScopeUuid]
|
||||
..sort((a, b) => a.uuid.compareTo(b.uuid));
|
||||
|
||||
expect(scopes, expectedScopes);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when member grants are resolved, '
|
||||
'then the personal and granted shared scopes are returned with their roles.',
|
||||
() async {
|
||||
final grants = await CrdtScopeMembership.memberGrants(session, userUuid);
|
||||
|
||||
expect(
|
||||
{for (final grant in grants) grant.uuidScopeId: grant.role},
|
||||
{
|
||||
userUuid: CrdtScopeRole.readWrite,
|
||||
sharedScopeUuid: CrdtScopeRole.readWrite,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when membership is checked for the personal scope, '
|
||||
'then the user is a member.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: userUuid,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when membership is checked for the granted shared scope, '
|
||||
'then the user is a member.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for the granted shared scope, '
|
||||
'then the stored membership role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for the personal scope, '
|
||||
'then the readWrite role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: userUuid,
|
||||
),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for a scope without a CRDT scope row, '
|
||||
'then no role is returned.',
|
||||
() async {
|
||||
final missingScopeUuid = const Uuid().v7obj();
|
||||
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: missingScopeUuid,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when a stray membership row grants readOnly access to the personal scope, '
|
||||
'then the personal grant remains readWrite.',
|
||||
() async {
|
||||
final personalScope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: userUuid),
|
||||
);
|
||||
await server.CrdtScopeMember.db.insertRow(
|
||||
session,
|
||||
server.CrdtScopeMember(
|
||||
scopeId: personalScope.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.CrdtScopeRole.readOnly,
|
||||
),
|
||||
);
|
||||
|
||||
final grants = await CrdtScopeMembership.memberGrants(session, userUuid);
|
||||
|
||||
expect(
|
||||
grants.singleWhere((grant) => grant.uuidScopeId == userUuid).role,
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when a stored membership row has no role, '
|
||||
'then resolving grants fails instead of treating it as a grant.',
|
||||
() async {
|
||||
final scope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: const Uuid().v7obj()),
|
||||
);
|
||||
final encodedScopeId = ValueEncoder.instance.convert(scope.id);
|
||||
final encodedUserUuid = ValueEncoder.instance.convert(userUuid);
|
||||
|
||||
await session.db.unsafeExecute(
|
||||
'ALTER TABLE "crdt_scope_members" ALTER COLUMN "role" DROP NOT NULL',
|
||||
);
|
||||
await session.db.unsafeExecute(
|
||||
'INSERT INTO "crdt_scope_members" ("scopeId", "userUuid", "role") '
|
||||
'VALUES ($encodedScopeId, $encodedUserUuid, NULL)',
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
CrdtScopeMembership.memberGrants(session, userUuid),
|
||||
throwsA(isA<Error>()),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('Given a user with readOnly membership in a shared CRDT scope,', () {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedScopeUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedScopeUuid = const Uuid().v7obj();
|
||||
final sharedScope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: sharedScopeUuid),
|
||||
);
|
||||
await server.CrdtScopeMember.db.insertRow(
|
||||
session,
|
||||
server.CrdtScopeMember(
|
||||
scopeId: sharedScope.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.CrdtScopeRole.readOnly,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when the role is resolved for that scope, '
|
||||
'then the readOnly role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
),
|
||||
CrdtScopeRole.readOnly,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a shared CRDT scope without membership for a user,', () {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedScopeUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedScopeUuid = const Uuid().v7obj();
|
||||
await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: sharedScopeUuid),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when membership is checked for that scope, '
|
||||
'then the user is not a member.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:serverpod_offline_sync_server/src/generated/protocol.dart' as server;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('[CRDT Space Membership]', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
|
||||
setUp(() {
|
||||
session = sessionBuilder.build();
|
||||
});
|
||||
|
||||
group(
|
||||
'Given a user with personal space access and readWrite membership in one shared CRDT space,',
|
||||
() {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedSpaceUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedSpaceUuid = const Uuid().v7obj();
|
||||
|
||||
final sharedSpace = await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: sharedSpaceUuid),
|
||||
);
|
||||
await server.OfflineSyncSpaceMember.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpaceMember(
|
||||
spaceId: sharedSpace.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.OfflineSyncSpaceRole.readWrite,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when member spaces are resolved, '
|
||||
'then only the personal space and the granted shared space are included.',
|
||||
() async {
|
||||
final spaces = await OfflineSyncSpaceMembership.memberSpaces(
|
||||
session,
|
||||
userUuid,
|
||||
);
|
||||
final expectedSpaces = [userUuid, sharedSpaceUuid]
|
||||
..sort((a, b) => a.uuid.compareTo(b.uuid));
|
||||
|
||||
expect(spaces, expectedSpaces);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when member grants are resolved, '
|
||||
'then the personal and granted shared spaces are returned with their roles.',
|
||||
() async {
|
||||
final grants = await OfflineSyncSpaceMembership.memberGrants(
|
||||
session,
|
||||
userUuid,
|
||||
);
|
||||
|
||||
expect(
|
||||
{for (final grant in grants) grant.uuidSpaceId: grant.role},
|
||||
{
|
||||
userUuid: OfflineSyncSpaceRole.readWrite,
|
||||
sharedSpaceUuid: OfflineSyncSpaceRole.readWrite,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when membership is checked for the personal space, '
|
||||
'then the user is a member.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: userUuid,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when membership is checked for the granted shared space, '
|
||||
'then the user is a member.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: sharedSpaceUuid,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for the granted shared space, '
|
||||
'then the stored membership role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: sharedSpaceUuid,
|
||||
),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for the personal space, '
|
||||
'then the readWrite role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: userUuid,
|
||||
),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for a space without a CRDT space row, '
|
||||
'then no role is returned.',
|
||||
() async {
|
||||
final missingSpaceUuid = const Uuid().v7obj();
|
||||
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: missingSpaceUuid,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when a stray membership row grants readOnly access to the personal space, '
|
||||
'then the personal grant remains readWrite.',
|
||||
() async {
|
||||
final personalSpace = await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: userUuid),
|
||||
);
|
||||
await server.OfflineSyncSpaceMember.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpaceMember(
|
||||
spaceId: personalSpace.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.OfflineSyncSpaceRole.readOnly,
|
||||
),
|
||||
);
|
||||
|
||||
final grants = await OfflineSyncSpaceMembership.memberGrants(
|
||||
session,
|
||||
userUuid,
|
||||
);
|
||||
|
||||
expect(
|
||||
grants.singleWhere((grant) => grant.uuidSpaceId == userUuid).role,
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when a stored membership row has no role, '
|
||||
'then resolving grants fails instead of treating it as a grant.',
|
||||
() async {
|
||||
final space = await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: const Uuid().v7obj()),
|
||||
);
|
||||
final encodedSpaceId = ValueEncoder.instance.convert(space.id);
|
||||
final encodedUserUuid = ValueEncoder.instance.convert(userUuid);
|
||||
|
||||
await session.db.unsafeExecute(
|
||||
'ALTER TABLE "offline_sync_space_members" ALTER COLUMN "role" DROP NOT NULL',
|
||||
);
|
||||
await session.db.unsafeExecute(
|
||||
'INSERT INTO "offline_sync_space_members" ("spaceId", "userUuid", "role") '
|
||||
'VALUES ($encodedSpaceId, $encodedUserUuid, NULL)',
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
OfflineSyncSpaceMembership.memberGrants(session, userUuid),
|
||||
throwsA(isA<Error>()),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('Given a user with readOnly membership in a shared CRDT space,', () {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedSpaceUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedSpaceUuid = const Uuid().v7obj();
|
||||
final sharedSpace = await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: sharedSpaceUuid),
|
||||
);
|
||||
await server.OfflineSyncSpaceMember.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpaceMember(
|
||||
spaceId: sharedSpace.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.OfflineSyncSpaceRole.readOnly,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when the role is resolved for that space, '
|
||||
'then the readOnly role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: sharedSpaceUuid,
|
||||
),
|
||||
OfflineSyncSpaceRole.readOnly,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a shared CRDT space without membership for a user,', () {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedSpaceUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedSpaceUuid = const Uuid().v7obj();
|
||||
await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: sharedSpaceUuid),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when membership is checked for that space, '
|
||||
'then the user is not a member.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: sharedSpaceUuid,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ void withServerpod(
|
|||
}
|
||||
|
||||
class TestEndpoints {
|
||||
late final _CrdtSyncEndpoint crdtSync;
|
||||
late final _OfflineSyncEndpoint offlineSync;
|
||||
}
|
||||
|
||||
class _InternalTestEndpoints extends TestEndpoints
|
||||
|
|
@ -154,15 +154,15 @@ class _InternalTestEndpoints extends TestEndpoints
|
|||
_is.SerializationManager serializationManager,
|
||||
_is.EndpointDispatch endpoints,
|
||||
) {
|
||||
crdtSync = _CrdtSyncEndpoint(
|
||||
offlineSync = _OfflineSyncEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CrdtSyncEndpoint {
|
||||
_CrdtSyncEndpoint(
|
||||
class _OfflineSyncEndpoint {
|
||||
_OfflineSyncEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
|
@ -171,23 +171,23 @@ class _CrdtSyncEndpoint {
|
|||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Stream<_icw2tu00.CrdtSyncStreamEvent> sync(
|
||||
_ida.Stream<_icw2tu00.OfflineSyncStreamEvent> sync(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _ida.Stream<_icw2tu00.CrdtSyncStreamEvent> changes,
|
||||
required _ida.Stream<_icw2tu00.OfflineSyncStreamEvent> changes,
|
||||
required bool once,
|
||||
}) {
|
||||
var _localTestStreamManager =
|
||||
_ist.TestStreamManager<_icw2tu00.CrdtSyncStreamEvent>();
|
||||
_ist.TestStreamManager<_icw2tu00.OfflineSyncStreamEvent>();
|
||||
_ist.callStreamFunctionAndHandleExceptions(
|
||||
() async {
|
||||
var _localUniqueSession = (sessionBuilder as _ist.InternalTestSessionBuilder)
|
||||
.internalBuild(
|
||||
endpoint: 'crdtSync',
|
||||
endpoint: 'offlineSync',
|
||||
method: 'sync',
|
||||
);
|
||||
var _localCallContext = await _endpointDispatch.getMethodStreamCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'crdtSync',
|
||||
endpointPath: 'offlineSync',
|
||||
methodName: 'sync',
|
||||
arguments: {'once': once},
|
||||
requestedInputStreams: ['changes'],
|
||||
|
|
|
|||
Loading…
Reference in a new issue