Browse Source

Task/remove deprecated permissions attribute from access (#7639)

* Remove deprecated permissions attribute from access

* Update changelog
pull/7675/head
Thomas Kaul 9 hours ago
committed by GitHub
parent
commit
17e1a2b5fc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 10
      apps/api/src/app/access/access.controller.ts
  3. 1
      apps/api/src/app/user/user.service.ts
  4. 42
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  5. 6
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  6. 3
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts
  7. 23
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  8. 12
      libs/common/src/lib/dtos/create-access.dto.ts
  9. 12
      libs/common/src/lib/dtos/update-access.dto.ts
  10. 8
      libs/common/src/lib/interfaces/access.interface.ts
  11. 2
      libs/common/src/lib/interfaces/user.interface.ts
  12. 57
      libs/common/src/lib/scopes.spec.ts
  13. 21
      libs/common/src/lib/scopes.ts
  14. 5
      prisma/migrations/20260820120000_removed_permissions_from_access/migration.sql
  15. 21
      prisma/schema.prisma

1
CHANGELOG.md

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the sorting to be case-insensitive in the account selector component - Improved the sorting to be case-insensitive in the account selector component
- Refactored the services to use the `@Service()` decorator of _Angular_ - Refactored the services to use the `@Service()` decorator of _Angular_
- Removed the deprecated `permissions` attribute of the access in favor of the scopes
## 3.55.0 - 2026-08-19 ## 3.55.0 - 2026-08-19

10
apps/api/src/app/access/access.controller.ts

@ -49,14 +49,13 @@ export class AccessController {
}); });
return accessesWithGranteeUser.map((accessItem) => { return accessesWithGranteeUser.map((accessItem) => {
const { alias, granteeUser, id, permissions, settings } = accessItem; const { alias, granteeUser, id, settings } = accessItem;
const scopes = getScopesOfAccess(accessItem); const scopes = getScopesOfAccess(accessItem);
if (granteeUser) { if (granteeUser) {
return { return {
alias, alias,
id, id,
permissions,
scopes, scopes,
grantee: granteeUser?.id, grantee: granteeUser?.id,
settings: settings as AccessSettings, settings: settings as AccessSettings,
@ -67,7 +66,6 @@ export class AccessController {
return { return {
alias, alias,
id, id,
permissions,
scopes, scopes,
grantee: 'Public', grantee: 'Public',
settings: settings as AccessSettings, settings: settings as AccessSettings,
@ -98,10 +96,9 @@ export class AccessController {
granteeUser: data.granteeUserId granteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } } ? { connect: { id: data.granteeUserId } }
: undefined, : undefined,
permissions: data.permissions,
scopes: getScopesOfAccess({ scopes: getScopesOfAccess({
granteeUserId: data.granteeUserId, granteeUserId: data.granteeUserId,
permissions: data.permissions scopes: data.scopes
}), }),
settings: this.accessService.buildSettings(data.filters), settings: this.accessService.buildSettings(data.filters),
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
@ -171,10 +168,9 @@ export class AccessController {
granteeUser: data.granteeUserId granteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } } ? { connect: { id: data.granteeUserId } }
: { disconnect: true }, : { disconnect: true },
permissions: data.permissions,
scopes: getScopesOfAccess({ scopes: getScopesOfAccess({
granteeUserId: data.granteeUserId, granteeUserId: data.granteeUserId,
permissions: data.permissions ?? originalAccess.permissions scopes: data.scopes ?? originalAccess.scopes
}), }),
settings: this.accessService.buildSettings(data.filters) settings: this.accessService.buildSettings(data.filters)
}, },

1
apps/api/src/app/user/user.service.ts

@ -220,7 +220,6 @@ export class UserService {
return { return {
alias: accessItem.alias, alias: accessItem.alias,
id: accessItem.id, id: accessItem.id,
permissions: accessItem.permissions,
scopes: getScopesOfAccess(accessItem) scopes: getScopesOfAccess(accessItem)
}; };
}), }),

42
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -1,6 +1,12 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import {
SCOPES_OF_READ_ACCESS,
SCOPES_OF_READ_RESTRICTED_ACCESS,
hasScope,
scopes
} from '@ghostfolio/common/scopes';
import { AccountWithPlatform } from '@ghostfolio/common/types'; import { AccountWithPlatform } from '@ghostfolio/common/types';
import { validateObjectForForm } from '@ghostfolio/common/utils'; import { validateObjectForForm } from '@ghostfolio/common/utils';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
@ -40,7 +46,6 @@ import {
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { AccessPermission } from '@prisma/client';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { EMPTY, catchError } from 'rxjs'; import { EMPTY, catchError } from 'rxjs';
@ -111,10 +116,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
access?.grantee ?? null, access?.grantee ?? null,
isPublic ? null : Validators.required isPublic ? null : Validators.required
], ],
permissions: [ hasScopeToReadValues: hasScope(
access?.permissions[0] ?? AccessPermission.READ_RESTRICTED, access?.scopes,
Validators.required scopes.portfolioReadValues
], ),
type: [ type: [
{ disabled: this.mode === 'update', value: access?.type ?? 'PRIVATE' }, { disabled: this.mode === 'update', value: access?.type ?? 'PRIVATE' },
Validators.required Validators.required
@ -139,7 +144,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((accessType) => { .subscribe((accessType) => {
const granteeUserIdControl = this.accessForm.get('granteeUserId'); const granteeUserIdControl = this.accessForm.get('granteeUserId');
const permissionsControl = this.accessForm.get('permissions');
const hasScopeToReadValuesControl = this.accessForm.get(
'hasScopeToReadValues'
);
if (accessType === 'PRIVATE') { if (accessType === 'PRIVATE') {
granteeUserIdControl?.setValidators(Validators.required); granteeUserIdControl?.setValidators(Validators.required);
@ -147,9 +155,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} else { } else {
granteeUserIdControl?.clearValidators(); granteeUserIdControl?.clearValidators();
granteeUserIdControl?.setValue(null); granteeUserIdControl?.setValue(null);
permissionsControl?.setValue(
access?.permissions[0] ?? AccessPermission.READ_RESTRICTED // A public access never exposes the monetary values
); hasScopeToReadValuesControl?.setValue(false);
} }
granteeUserIdControl?.updateValueAndValidity(); granteeUserIdControl?.updateValueAndValidity();
@ -178,6 +186,18 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
); );
} }
/**
* The dialog offers the read access only. The write scopes are not granted
* here yet.
*/
private buildScopes() {
return [
...(this.accessForm.get('hasScopeToReadValues')?.value
? SCOPES_OF_READ_ACCESS
: SCOPES_OF_READ_RESTRICTED_ACCESS)
];
}
private async createAccess() { private async createAccess() {
const filters = this.buildFilters(); const filters = this.buildFilters();
@ -185,7 +205,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
alias: this.accessForm.get('alias')?.value, alias: this.accessForm.get('alias')?.value,
filters: filters.length > 0 ? filters : undefined, filters: filters.length > 0 ? filters : undefined,
granteeUserId: this.accessForm.get('granteeUserId')?.value, granteeUserId: this.accessForm.get('granteeUserId')?.value,
permissions: [this.accessForm.get('permissions')?.value] scopes: this.buildScopes()
}; };
try { try {
@ -244,7 +264,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
filters: filters.length > 0 ? filters : undefined, filters: filters.length > 0 ? filters : undefined,
granteeUserId: this.accessForm.get('granteeUserId')?.value, granteeUserId: this.accessForm.get('granteeUserId')?.value,
id: accessId, id: accessId,
permissions: [this.accessForm.get('permissions')?.value] scopes: this.buildScopes()
}; };
try { try {

6
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

@ -36,10 +36,10 @@
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Permission</mat-label> <mat-label i18n>Permission</mat-label>
<mat-select formControlName="permissions"> <mat-select formControlName="hasScopeToReadValues">
<mat-option i18n value="READ_RESTRICTED">Restricted view</mat-option> <mat-option i18n [value]="false">Restricted view</mat-option>
@if (accessForm.get('type')?.value === 'PRIVATE') { @if (accessForm.get('type')?.value === 'PRIVATE') {
<mat-option i18n value="READ">View</mat-option> <mat-option i18n [value]="true">View</mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>

3
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts

@ -1,6 +1,5 @@
import { Access } from '@ghostfolio/common/interfaces'; import { Access } from '@ghostfolio/common/interfaces';
export interface CreateOrUpdateAccessDialogParams { export interface CreateOrUpdateAccessDialogParams {
// TODO: Include the scopes once the dialog allows to configure them access?: Access;
access?: Omit<Access, 'scopes'>;
} }

23
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -228,7 +228,7 @@ export class GfUserAccountAccessComponent implements OnInit {
alias: access.alias, alias: access.alias,
grantee: access.grantee === 'Public' ? undefined : access.grantee, grantee: access.grantee === 'Public' ? undefined : access.grantee,
id: access.id, id: access.id,
permissions: access.permissions, scopes: access.scopes,
settings: access.settings, settings: access.settings,
type: access.type type: access.type
} }
@ -247,18 +247,15 @@ export class GfUserAccountAccessComponent implements OnInit {
} }
private update() { private update() {
this.accessesGet = this.user.access.map( this.accessesGet = this.user.access.map(({ alias, id, scopes }) => {
({ alias, id, permissions, scopes }) => { return {
return { id,
id, scopes,
permissions, alias: alias ?? '',
scopes, grantee: $localize`Me`,
alias: alias ?? '', type: 'PRIVATE'
grantee: $localize`Me`, };
type: 'PRIVATE' });
};
}
);
this.dataService this.dataService
.fetchAccesses() .fetchAccesses()

12
libs/common/src/lib/dtos/create-access.dto.ts

@ -1,7 +1,7 @@
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter } from '@ghostfolio/common/interfaces';
import { Scope, scopes } from '@ghostfolio/common/scopes';
import { AccessPermission } from '@prisma/client'; import { IsArray, IsIn, IsOptional, IsString, IsUUID } from 'class-validator';
import { IsArray, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
export class CreateAccessDto { export class CreateAccessDto {
@IsOptional() @IsOptional()
@ -16,10 +16,8 @@ export class CreateAccessDto {
@IsUUID() @IsUUID()
granteeUserId?: string; granteeUserId?: string;
/** @IsArray()
* @deprecated Use the scopes instead @IsIn(Object.values(scopes), { each: true })
*/
@IsEnum(AccessPermission, { each: true })
@IsOptional() @IsOptional()
permissions?: AccessPermission[]; scopes?: Scope[];
} }

12
libs/common/src/lib/dtos/update-access.dto.ts

@ -1,7 +1,7 @@
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter } from '@ghostfolio/common/interfaces';
import { Scope, scopes } from '@ghostfolio/common/scopes';
import { AccessPermission } from '@prisma/client'; import { IsArray, IsIn, IsOptional, IsString, IsUUID } from 'class-validator';
import { IsArray, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
export class UpdateAccessDto { export class UpdateAccessDto {
@IsOptional() @IsOptional()
@ -19,10 +19,8 @@ export class UpdateAccessDto {
@IsString() @IsString()
id: string; id: string;
/** @IsArray()
* @deprecated Use the scopes instead @IsIn(Object.values(scopes), { each: true })
*/
@IsEnum(AccessPermission, { each: true })
@IsOptional() @IsOptional()
permissions?: AccessPermission[]; scopes?: Scope[];
} }

8
libs/common/src/lib/interfaces/access.interface.ts

@ -1,19 +1,11 @@
import { AccessType } from '@ghostfolio/common/types'; import { AccessType } from '@ghostfolio/common/types';
import { AccessPermission } from '@prisma/client';
import { AccessSettings } from './access-settings.interface'; import { AccessSettings } from './access-settings.interface';
export interface Access { export interface Access {
alias: string | null; alias: string | null;
grantee?: string; grantee?: string;
id: string; id: string;
/**
* @deprecated Use the scopes instead
*/
permissions: AccessPermission[];
scopes: string[]; scopes: string[];
settings?: AccessSettings; settings?: AccessSettings;
type: AccessType; type: AccessType;

2
libs/common/src/lib/interfaces/user.interface.ts

@ -10,7 +10,7 @@ import { UserSettings } from './user-settings.interface';
// TODO: Compare with UserWithSettings // TODO: Compare with UserWithSettings
export interface User { export interface User {
access: Pick<Access, 'alias' | 'id' | 'permissions' | 'scopes'>[]; access: Pick<Access, 'alias' | 'id' | 'scopes'>[];
accounts: AccountWithPlatform[]; accounts: AccountWithPlatform[];
activitiesCount: number; activitiesCount: number;
dateOfFirstActivity: Date; dateOfFirstActivity: Date;

57
libs/common/src/lib/scopes.spec.ts

@ -1,5 +1,6 @@
import { import {
SCOPES_OF_READ_ACCESS, SCOPES_OF_READ_ACCESS,
SCOPES_OF_READ_RESTRICTED_ACCESS,
SCOPES_OF_WRITE_ACCESS, SCOPES_OF_WRITE_ACCESS,
getScopesOfAccess, getScopesOfAccess,
getScopesOfOwnAccess, getScopesOfOwnAccess,
@ -11,7 +12,7 @@ import {
describe('Scopes', () => { describe('Scopes', () => {
describe('Scopes of read access', () => { describe('Scopes of read access', () => {
// A new scope which reads data has to be added here deliberately, because // A new scope which reads data has to be added here deliberately, because
// an access with the permission to read receives this list // an access which reads data receives this list
it('Covers every read scope', () => { it('Covers every read scope', () => {
expect(SCOPES_OF_READ_ACCESS).toEqual([ expect(SCOPES_OF_READ_ACCESS).toEqual([
scopes.accountRead, scopes.accountRead,
@ -55,68 +56,56 @@ describe('Scopes', () => {
}); });
describe('Get scopes of access', () => { describe('Get scopes of access', () => {
it('Scopes take precedence over the permissions', () => { it('Gives the scopes of the access', () => {
expect( expect(
getScopesOfAccess({ getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d', granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
permissions: ['READ'], scopes: [scopes.portfolioRead, scopes.portfolioReadValues]
scopes: [scopes.portfolioRead]
}) })
).toEqual([scopes.portfolioRead]); ).toEqual([scopes.portfolioRead, scopes.portfolioReadValues]);
}); });
it('Derive from the permission to read', () => { it('Without the scope to read the values', () => {
// An access created before the scopes have been introduced has no scopes
expect( expect(
getScopesOfAccess({ getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d', granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
permissions: ['READ'], scopes: [scopes.portfolioRead]
scopes: []
}) })
).toContain(scopes.portfolioReadValues); ).not.toContain(scopes.portfolioReadValues);
}); });
it('Derive from the permission to read restricted', () => { it('Without scopes', () => {
expect( expect(
getScopesOfAccess({ getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d', granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d'
permissions: ['READ_RESTRICTED'],
scopes: []
}) })
).not.toContain(scopes.portfolioReadValues); ).toEqual([]);
}); });
it('The permission to read gives no write scope', () => { // TODO: Remove this expectation once the dialog allows to configure the
// write scopes
it('Gives no write scope', () => {
const scopesOfAccess = getScopesOfAccess({ const scopesOfAccess = getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d', granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
permissions: ['READ'], scopes: [...SCOPES_OF_READ_ACCESS, ...SCOPES_OF_WRITE_ACCESS]
scopes: []
}); });
for (const scope of SCOPES_OF_WRITE_ACCESS) { for (const scope of SCOPES_OF_WRITE_ACCESS) {
expect(scopesOfAccess).not.toContain(scope); expect(scopesOfAccess).not.toContain(scope);
} }
}); });
it('Without permissions and scopes', () => {
expect(
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d'
})
).not.toContain(scopes.portfolioReadValues);
});
}); });
describe('Get scopes of public access', () => { describe('Get scopes of public access', () => {
it('Allows reading the portfolio', () => { it('Allows reading the portfolio', () => {
expect(getScopesOfAccess({ permissions: ['READ_RESTRICTED'] })).toContain( expect(
scopes.portfolioRead getScopesOfAccess({ scopes: [...SCOPES_OF_READ_RESTRICTED_ACCESS] })
); ).toContain(scopes.portfolioRead);
}); });
it('Excludes the accounts and the watchlist', () => { it('Excludes the accounts and the watchlist', () => {
const scopesOfAccess = getScopesOfAccess({ const scopesOfAccess = getScopesOfAccess({
permissions: ['READ_RESTRICTED'] scopes: [...SCOPES_OF_READ_RESTRICTED_ACCESS]
}); });
expect(scopesOfAccess).not.toContain(scopes.accountRead); expect(scopesOfAccess).not.toContain(scopes.accountRead);
@ -135,10 +124,10 @@ describe('Scopes', () => {
).toEqual([scopes.portfolioRead]); ).toEqual([scopes.portfolioRead]);
}); });
it('Cannot be widened by the permission to read', () => { it('Cannot expose the monetary values', () => {
expect(getScopesOfAccess({ permissions: ['READ'] })).not.toContain( expect(
scopes.portfolioReadValues getScopesOfAccess({ scopes: [...SCOPES_OF_READ_ACCESS] })
); ).not.toContain(scopes.portfolioReadValues);
}); });
}); });

21
libs/common/src/lib/scopes.ts

@ -1,5 +1,3 @@
import { AccessPermission } from '@prisma/client';
/** /**
* Scopes describe what a grantee may do on behalf of the granting user. They * Scopes describe what a grantee may do on behalf of the granting user. They
* are a separate axis from the permissions, which describe the capabilities of * are a separate axis from the permissions, which describe the capabilities of
@ -54,32 +52,25 @@ const SCOPES_OF_PUBLIC_ACCESS: readonly Scope[] = [
scopes.portfolioRead scopes.portfolioRead
]; ];
const SCOPES_OF_READ_RESTRICTED_ACCESS: readonly Scope[] = export const SCOPES_OF_READ_RESTRICTED_ACCESS: readonly Scope[] =
SCOPES_OF_READ_ACCESS.filter((scope) => { SCOPES_OF_READ_ACCESS.filter((scope) => {
return scope !== scopes.portfolioReadValues; return scope !== scopes.portfolioReadValues;
}); });
export function getScopesOfAccess({ export function getScopesOfAccess({
granteeUserId, granteeUserId,
permissions,
scopes: scopesOfAccess scopes: scopesOfAccess
}: { }: {
granteeUserId?: string | null; granteeUserId?: string | null;
permissions?: AccessPermission[];
scopes?: string[]; scopes?: string[];
}): string[] { }): string[] {
let scopesToEvaluate: readonly string[] = scopesOfAccess ?? []; const scopesToEvaluate = scopesOfAccess ?? [];
if (!scopesToEvaluate.length) {
// TODO: Remove the derivation from the permissions once they have been
// dropped from the access
scopesToEvaluate = permissions?.includes('READ')
? SCOPES_OF_READ_ACCESS
: SCOPES_OF_READ_RESTRICTED_ACCESS;
}
if (granteeUserId) { if (granteeUserId) {
return [...scopesToEvaluate]; // TODO: Permit the write scopes once the dialog allows to configure them
return SCOPES_OF_READ_ACCESS.filter((scope) => {
return scopesToEvaluate.includes(scope);
});
} }
// An access which has not been granted to a user is public, hence it is // An access which has not been granted to a user is public, hence it is

5
prisma/migrations/20260820120000_removed_permissions_from_access/migration.sql

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Access" DROP COLUMN "permissions";
-- DropEnum
DROP TYPE "AccessPermission";

21
prisma/schema.prisma

@ -10,17 +10,15 @@ datasource db {
model Access { model Access {
alias String? alias String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
granteeUser User? @relation("accessGet", fields: [granteeUserId], onDelete: Cascade, references: [id]) granteeUser User? @relation("accessGet", fields: [granteeUserId], onDelete: Cascade, references: [id])
granteeUserId String? granteeUserId String?
id String @id @default(uuid()) id String @id @default(uuid())
/// @deprecated Use the scopes instead scopes String[] @default([])
permissions AccessPermission[] @default([READ_RESTRICTED]) settings Json @default("{}")
scopes String[] @default([]) updatedAt DateTime @updatedAt
settings Json @default("{}")
updatedAt DateTime @updatedAt
userId String userId String
user User @relation("accessGive", fields: [userId], onDelete: Cascade, references: [id]) user User @relation("accessGive", fields: [userId], onDelete: Cascade, references: [id])
@@index([alias]) @@index([alias])
@@index([granteeUserId]) @@index([granteeUserId])
@ -333,11 +331,6 @@ model User {
@@index([thirdPartyId]) @@index([thirdPartyId])
} }
enum AccessPermission {
READ
READ_RESTRICTED
}
enum AssetClass { enum AssetClass {
ALTERNATIVE_INVESTMENT ALTERNATIVE_INVESTMENT
COMMODITY COMMODITY

Loading…
Cancel
Save