mirror of
https://github.com/samsonjs/immich.git
synced 2026-03-25 09:15:56 +00:00
feat: sync auth user (#20067)
This commit is contained in:
parent
ab597155fa
commit
92384c28de
20 changed files with 269 additions and 40 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 18736fc27a80c99c68e856cdb4f842bc81ed3445
|
||||
Subproject commit 37f60ea537c0228f5f92e4f42dc42f0bb39a6d7f
|
||||
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/sync_auth_user_v1.dart
generated
Normal file
BIN
mobile/openapi/lib/model/sync_auth_user_v1.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/sync_entity_type.dart
generated
BIN
mobile/openapi/lib/model/sync_entity_type.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/sync_request_type.dart
generated
BIN
mobile/openapi/lib/model/sync_request_type.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/sync_user_v1.dart
generated
BIN
mobile/openapi/lib/model/sync_user_v1.dart
generated
Binary file not shown.
2
mobile/test/fixtures/sync_stream.stub.dart
vendored
2
mobile/test/fixtures/sync_stream.stub.dart
vendored
|
|
@ -9,6 +9,7 @@ abstract final class SyncStreamStub {
|
|||
email: "admin@admin",
|
||||
id: "1",
|
||||
name: "Admin",
|
||||
avatarColor: null,
|
||||
),
|
||||
ack: "1",
|
||||
);
|
||||
|
|
@ -19,6 +20,7 @@ abstract final class SyncStreamStub {
|
|||
email: "user@user",
|
||||
id: "5",
|
||||
name: "User",
|
||||
avatarColor: null,
|
||||
),
|
||||
ack: "5",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13978,8 +13978,79 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAuthUserV1": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/UserAvatarColor"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"deletedAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"hasProfileImage": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"oauthId": {
|
||||
"type": "string"
|
||||
},
|
||||
"pinCode": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"profileChangedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"quotaSizeInBytes": {
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"quotaUsageInBytes": {
|
||||
"type": "integer"
|
||||
},
|
||||
"storageLabel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"avatarColor",
|
||||
"deletedAt",
|
||||
"email",
|
||||
"hasProfileImage",
|
||||
"id",
|
||||
"isAdmin",
|
||||
"name",
|
||||
"oauthId",
|
||||
"pinCode",
|
||||
"profileChangedAt",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"storageLabel"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncEntityType": {
|
||||
"enum": [
|
||||
"AuthUserV1",
|
||||
"UserV1",
|
||||
"UserDeleteV1",
|
||||
"AssetV1",
|
||||
|
|
@ -14242,6 +14313,7 @@
|
|||
"AlbumAssetExifsV1",
|
||||
"AssetsV1",
|
||||
"AssetExifsV1",
|
||||
"AuthUsersV1",
|
||||
"MemoriesV1",
|
||||
"MemoryToAssetsV1",
|
||||
"PartnersV1",
|
||||
|
|
@ -14364,6 +14436,14 @@
|
|||
},
|
||||
"SyncUserV1": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/UserAvatarColor"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"deletedAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
|
|
@ -14380,6 +14460,7 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"avatarColor",
|
||||
"deletedAt",
|
||||
"email",
|
||||
"id",
|
||||
|
|
|
|||
|
|
@ -4099,6 +4099,7 @@ export enum Error2 {
|
|||
NotFound = "not_found"
|
||||
}
|
||||
export enum SyncEntityType {
|
||||
AuthUserV1 = "AuthUserV1",
|
||||
UserV1 = "UserV1",
|
||||
UserDeleteV1 = "UserDeleteV1",
|
||||
AssetV1 = "AssetV1",
|
||||
|
|
@ -4149,6 +4150,7 @@ export enum SyncRequestType {
|
|||
AlbumAssetExifsV1 = "AlbumAssetExifsV1",
|
||||
AssetsV1 = "AssetsV1",
|
||||
AssetExifsV1 = "AssetExifsV1",
|
||||
AuthUsersV1 = "AuthUsersV1",
|
||||
MemoriesV1 = "MemoriesV1",
|
||||
MemoryToAssetsV1 = "MemoryToAssetsV1",
|
||||
PartnersV1 = "PartnersV1",
|
||||
|
|
|
|||
|
|
@ -356,6 +356,7 @@ export const columns = {
|
|||
],
|
||||
syncAlbumUser: ['album_user.albumsId as albumId', 'album_user.usersId as userId', 'album_user.role'],
|
||||
syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'],
|
||||
syncUser: ['id', 'name', 'email', 'avatarColor', 'deletedAt', 'updateId'],
|
||||
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
|
||||
syncAssetExif: [
|
||||
'asset_exif.assetId',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
MemoryType,
|
||||
SyncEntityType,
|
||||
SyncRequestType,
|
||||
UserAvatarColor,
|
||||
UserMetadataKey,
|
||||
} from 'src/enum';
|
||||
import { UserMetadata } from 'src/types';
|
||||
|
|
@ -58,9 +59,25 @@ export class SyncUserV1 {
|
|||
id!: string;
|
||||
name!: string;
|
||||
email!: string;
|
||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', nullable: true })
|
||||
avatarColor!: UserAvatarColor | null;
|
||||
deletedAt!: Date | null;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAuthUserV1 extends SyncUserV1 {
|
||||
isAdmin!: boolean;
|
||||
pinCode!: string | null;
|
||||
oauthId!: string;
|
||||
storageLabel!: string | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
quotaSizeInBytes!: number | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
quotaUsageInBytes!: number;
|
||||
hasProfileImage!: boolean;
|
||||
profileChangedAt!: Date;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncUserDeleteV1 {
|
||||
userId!: string;
|
||||
|
|
@ -301,6 +318,7 @@ export class SyncAckV1 {}
|
|||
export class SyncResetV1 {}
|
||||
|
||||
export type SyncItem = {
|
||||
[SyncEntityType.AuthUserV1]: SyncAuthUserV1;
|
||||
[SyncEntityType.UserV1]: SyncUserV1;
|
||||
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
||||
[SyncEntityType.PartnerV1]: SyncPartnerV1;
|
||||
|
|
|
|||
|
|
@ -559,6 +559,7 @@ export enum SyncRequestType {
|
|||
AlbumAssetExifsV1 = 'AlbumAssetExifsV1',
|
||||
AssetsV1 = 'AssetsV1',
|
||||
AssetExifsV1 = 'AssetExifsV1',
|
||||
AuthUsersV1 = 'AuthUsersV1',
|
||||
MemoriesV1 = 'MemoriesV1',
|
||||
MemoryToAssetsV1 = 'MemoryToAssetsV1',
|
||||
PartnersV1 = 'PartnersV1',
|
||||
|
|
@ -573,6 +574,8 @@ export enum SyncRequestType {
|
|||
}
|
||||
|
||||
export enum SyncEntityType {
|
||||
AuthUserV1 = 'AuthUserV1',
|
||||
|
||||
UserV1 = 'UserV1',
|
||||
UserDeleteV1 = 'UserDeleteV1',
|
||||
|
||||
|
|
|
|||
|
|
@ -444,6 +444,29 @@ where
|
|||
order by
|
||||
"asset_face"."updateId" asc
|
||||
|
||||
-- SyncRepository.authUser.getUpserts
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"deletedAt",
|
||||
"updateId",
|
||||
"isAdmin",
|
||||
"pinCode",
|
||||
"oauthId",
|
||||
"storageLabel",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"updatedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.memory.getDeletes
|
||||
select
|
||||
"id",
|
||||
|
|
@ -871,6 +894,7 @@ select
|
|||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"deletedAt",
|
||||
"updateId"
|
||||
from
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export class SyncRepository {
|
|||
asset: AssetSync;
|
||||
assetExif: AssetExifSync;
|
||||
assetFace: AssetFaceSync;
|
||||
authUser: AuthUserSync;
|
||||
memory: MemorySync;
|
||||
memoryToAsset: MemoryToAssetSync;
|
||||
partner: PartnerSync;
|
||||
|
|
@ -63,6 +64,7 @@ export class SyncRepository {
|
|||
this.asset = new AssetSync(this.db);
|
||||
this.assetExif = new AssetExifSync(this.db);
|
||||
this.assetFace = new AssetFaceSync(this.db);
|
||||
this.authUser = new AuthUserSync(this.db);
|
||||
this.memory = new MemorySync(this.db);
|
||||
this.memoryToAsset = new MemoryToAssetSync(this.db);
|
||||
this.partner = new PartnerSync(this.db);
|
||||
|
|
@ -367,6 +369,27 @@ class AssetSync extends BaseSync {
|
|||
}
|
||||
}
|
||||
|
||||
class AuthUserSync extends BaseSync {
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
getUpserts(ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('user')
|
||||
.select(columns.syncUser)
|
||||
.select([
|
||||
'isAdmin',
|
||||
'pinCode',
|
||||
'oauthId',
|
||||
'storageLabel',
|
||||
'quotaSizeInBytes',
|
||||
'quotaUsageInBytes',
|
||||
'profileImagePath',
|
||||
'profileChangedAt',
|
||||
])
|
||||
.$call(this.upsertTableFilters(ack))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class PersonSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getDeletes(userId: string, ack?: SyncAck) {
|
||||
|
|
@ -693,11 +716,7 @@ class UserSync extends BaseSync {
|
|||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
getUpserts(ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('user')
|
||||
.select(['id', 'name', 'email', 'deletedAt', 'updateId'])
|
||||
.$call(this.upsertTableFilters(ack))
|
||||
.stream();
|
||||
return this.db.selectFrom('user').select(columns.syncUser).$call(this.upsertTableFilters(ack)).stream();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ const sendEntityBackfillCompleteAck = (response: Writable, ackType: SyncEntityTy
|
|||
|
||||
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
|
||||
export const SYNC_TYPES_ORDER = [
|
||||
SyncRequestType.AuthUsersV1,
|
||||
SyncRequestType.UsersV1,
|
||||
SyncRequestType.PartnersV1,
|
||||
SyncRequestType.AssetsV1,
|
||||
|
|
@ -140,6 +141,7 @@ export class SyncService extends BaseService {
|
|||
const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)]));
|
||||
|
||||
const handlers: Record<SyncRequestType, () => Promise<void>> = {
|
||||
[SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(response, checkpointMap),
|
||||
[SyncRequestType.UsersV1]: () => this.syncUsersV1(response, checkpointMap),
|
||||
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(response, checkpointMap, auth),
|
||||
|
|
@ -169,6 +171,14 @@ export class SyncService extends BaseService {
|
|||
response.end();
|
||||
}
|
||||
|
||||
private async syncAuthUsersV1(response: Writable, checkpointMap: CheckpointMap) {
|
||||
const upsertType = SyncEntityType.AuthUserV1;
|
||||
const upserts = this.syncRepository.authUser.getUpserts(checkpointMap[upsertType]);
|
||||
for await (const { updateId, profileImagePath, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data: { ...data, hasProfileImage: !!profileImagePath } });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncUsersV1(response: Writable, checkpointMap: CheckpointMap) {
|
||||
const deleteType = SyncEntityType.UserDeleteV1;
|
||||
const deletes = this.syncRepository.user.getDeletes(checkpointMap[deleteType]);
|
||||
|
|
|
|||
|
|
@ -507,7 +507,14 @@ const userInsert = (user: Partial<Insertable<UserTable>> = {}) => {
|
|||
deletedAt: null,
|
||||
isAdmin: false,
|
||||
profileImagePath: '',
|
||||
profileChangedAt: newDate(),
|
||||
shouldChangePassword: true,
|
||||
storageLabel: null,
|
||||
pinCode: null,
|
||||
oauthId: '',
|
||||
avatarColor: null,
|
||||
quotaSizeInBytes: null,
|
||||
quotaUsageInBytes: 0,
|
||||
};
|
||||
|
||||
return { ...defaults, ...user, id };
|
||||
|
|
|
|||
87
server/test/medium/specs/sync/sync-auth-user.spec.ts
Normal file
87
server/test/medium/specs/sync/sync-auth-user.spec.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { Kysely } from 'kysely';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncEntityType.AuthUserV1, () => {
|
||||
it('should detect and sync the first user', async () => {
|
||||
const { auth, user, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: user.id,
|
||||
isAdmin: user.isAdmin,
|
||||
deletedAt: user.deletedAt,
|
||||
name: user.name,
|
||||
avatarColor: user.avatarColor,
|
||||
email: user.email,
|
||||
pinCode: user.pinCode,
|
||||
hasProfileImage: false,
|
||||
profileChangedAt: (user.profileChangedAt as Date).toISOString(),
|
||||
oauthId: user.oauthId,
|
||||
quotaSizeInBytes: user.quotaSizeInBytes,
|
||||
quotaUsageInBytes: user.quotaUsageInBytes,
|
||||
storageLabel: user.storageLabel,
|
||||
},
|
||||
type: 'AuthUserV1',
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AuthUsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should sync a change and then another change to that same user', async () => {
|
||||
const { auth, user, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: user.id,
|
||||
isAdmin: false,
|
||||
}),
|
||||
type: 'AuthUserV1',
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
await userRepo.update(user.id, { isAdmin: true });
|
||||
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: user.id,
|
||||
isAdmin: true,
|
||||
}),
|
||||
type: 'AuthUserV1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -37,6 +37,7 @@ describe(SyncEntityType.UserV1, () => {
|
|||
email: user.email,
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
avatarColor: user.avatarColor,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
|
|
@ -49,8 +50,7 @@ describe(SyncEntityType.UserV1, () => {
|
|||
it('should detect and sync a soft deleted user', async () => {
|
||||
const { auth, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const deletedAt = new Date().toISOString();
|
||||
const { user: deleted } = await ctx.newUser({ deletedAt });
|
||||
const { user: deleted } = await ctx.newUser({ deletedAt: new Date().toISOString() });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
|
|
@ -59,22 +59,12 @@ describe(SyncEntityType.UserV1, () => {
|
|||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
data: expect.objectContaining({ id: auth.user.id }),
|
||||
type: 'UserV1',
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt,
|
||||
email: deleted.email,
|
||||
id: deleted.id,
|
||||
name: deleted.name,
|
||||
},
|
||||
data: expect.objectContaining({ id: deleted.id }),
|
||||
type: 'UserV1',
|
||||
},
|
||||
]),
|
||||
|
|
@ -85,7 +75,7 @@ describe(SyncEntityType.UserV1, () => {
|
|||
});
|
||||
|
||||
it('should detect and sync a deleted user', async () => {
|
||||
const { auth, ctx } = await setup(await getKyselyDB());
|
||||
const { auth, user: authUser, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
|
||||
|
|
@ -104,12 +94,7 @@ describe(SyncEntityType.UserV1, () => {
|
|||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
data: expect.objectContaining({ id: authUser.id }),
|
||||
type: 'UserV1',
|
||||
},
|
||||
]);
|
||||
|
|
@ -119,7 +104,7 @@ describe(SyncEntityType.UserV1, () => {
|
|||
});
|
||||
|
||||
it('should sync a user and then an update to that same user', async () => {
|
||||
const { auth, ctx } = await setup(await getKyselyDB());
|
||||
const { auth, user, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
|
||||
|
|
@ -128,12 +113,7 @@ describe(SyncEntityType.UserV1, () => {
|
|||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
data: expect.objectContaining({ id: user.id }),
|
||||
type: 'UserV1',
|
||||
},
|
||||
]);
|
||||
|
|
@ -147,12 +127,7 @@ describe(SyncEntityType.UserV1, () => {
|
|||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: updated.name,
|
||||
},
|
||||
data: expect.objectContaining({ id: user.id, name: updated.name }),
|
||||
type: 'UserV1',
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue