Skip to content

Automagically declare Props/SecuredProps with TS transformer #3411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@
"ts-morph": "^25.0.1",
"ts-node": "^10.9.1",
"ts-patch": "^3.0.2",
"ts-transformer-keys": "^0.4.4",
"type-fest": "^4.15.0",
"typescript": "~5.8.3",
"typescript-transform-paths": "^3.4.6"
Expand Down
7 changes: 0 additions & 7 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Module } from '@nestjs/common';
import process from 'node:process';
import { assert } from 'ts-essentials';
import { keys } from 'ts-transformer-keys';
import { AdminModule } from './components/admin/admin.module';
import { AuthenticationModule } from './components/authentication/authentication.module';
import { AuthorizationModule } from './components/authorization/authorization.module';
Expand Down Expand Up @@ -45,11 +43,6 @@ import { CoreModule, LoggerModule } from './core';

import '@seedcompany/nest/patches';

assert(
keys<{ foo: string }>().length === 1,
'Sanity check for key transformer failed',
);

if (process.env.NODE_ENV !== 'production') {
Error.stackTraceLimit = Infinity;
}
Expand Down
11 changes: 0 additions & 11 deletions src/common/keysOf.spec.ts

This file was deleted.

24 changes: 16 additions & 8 deletions src/common/resource.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
import { createMetadataDecorator } from '@seedcompany/nest';
import { LazyGetter as Once } from 'lazy-get-decorator';
import { DateTime } from 'luxon';
import { keys as keysOf } from 'ts-transformer-keys';
import type {
ResourceDBMap,
ResourceLike,
Expand Down Expand Up @@ -53,9 +52,6 @@ export const resolveByTypename =
})
@DbLabel('BaseNode')
export abstract class Resource extends DataObject {
static readonly Props: string[] = keysOf<Resource>();
static readonly SecuredProps: string[] = [];

readonly __typename?: string;

@IdField()
Expand All @@ -77,8 +73,8 @@ export abstract class Resource extends DataObject {
type Thunk<T> = T | (() => T);

export type ResourceShape<T> = AbstractClassType<T> & {
Props: string[];
SecuredProps: string[];
Props?: string[];
SecuredProps?: string[];
// An optional list of props that exist on the BaseNode in the DB.
// Default should probably be considered the props on Resource class.
BaseNodeProps?: string[];
Expand Down Expand Up @@ -195,12 +191,24 @@ export class EnhancedResource<T extends ResourceShape<any>> {

@Once()
get props(): ReadonlySet<keyof T['prototype'] & string> {
return new Set<keyof T['prototype'] & string>(this.type.Props as any);
const props = this.type.Props;
if (!props) {
throw new Error(
`${this.name} has no props declared.\n\nDecorate with @RegisterResource or a GraphQL type decorator and move it to a file named: *.dto.ts.`,
);
}
return new Set<keyof T['prototype'] & string>(props);
}

@Once()
get securedProps(): ReadonlySet<SecuredResourceKey<T, false>> {
return new Set<SecuredResourceKey<T, false>>(this.type.SecuredProps as any);
const props = this.type.SecuredProps;
if (!props) {
throw new Error(
`${this.name} has no props declared.\n\nDecorate with @RegisterResource or a GraphQL type decorator and move it to a file named: *.dto.ts.`,
);
}
return new Set<SecuredResourceKey<T, false>>(props as any);
}

@Once()
Expand Down
2 changes: 1 addition & 1 deletion src/components/authentication/authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ForgotPassword } from '~/core/email/templates';
import { disableAccessPolicies, Gel } from '~/core/gel';
import { Privileges } from '../authorization';
import { rolesForScope, withoutScope } from '../authorization/dto';
import { AssignableRoles } from '../authorization/dto/assignable-roles';
import { AssignableRoles } from '../authorization/dto/assignable-roles.dto';
import { SystemAgentRepository } from '../user/system-agent.repository';
import { AuthenticationRepository } from './authentication.repository';
import { CryptoService } from './crypto.service';
Expand Down
2 changes: 1 addition & 1 deletion src/components/authorization/assignable-roles.granter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Role } from '~/common';
import { AssignableRoles } from './dto/assignable-roles';
import { AssignableRoles } from './dto/assignable-roles.dto';
import { Granter, ResourceGranter } from './policy';

@Granter(AssignableRoles)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import { RegisterResource } from '~/core/resources';
@RegisterResource()
@Calculated()
export class AssignableRoles {
static Props = [];
static SecuredProps = [];
static Relations = mapValues.fromList(Role, () => undefined)
.asRecord satisfies ResourceRelationsShape;
}
Expand Down
4 changes: 0 additions & 4 deletions src/components/authorization/dto/beta-features.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import { Calculated, type ResourceRelationsShape } from '~/common';
import { RegisterResource } from '~/core/resources';
import { Granter, ResourceGranter } from '../policy';
Expand All @@ -10,9 +9,6 @@ import { Granter, ResourceGranter } from '../policy';
@Calculated()
@ObjectType()
export class BetaFeatures {
static readonly Props = keysOf<BetaFeatures>();
static readonly SecuredProps = [];

// Declaring as relations as well so privileges can use.
static readonly Relations = {
projectChangeRequests: undefined,
Expand Down
4 changes: 0 additions & 4 deletions src/components/budget/dto/budget-record.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import {
Calculated,
type ID,
Expand All @@ -8,7 +7,6 @@ import {
type Secured,
SecuredFloatNullable,
SecuredInt,
type SecuredProps,
Sensitivity,
SensitivityField,
} from '~/common';
Expand All @@ -27,8 +25,6 @@ const Interfaces = IntersectTypes(Resource, ChangesetAware);
implements: Interfaces.members,
})
export class BudgetRecord extends Interfaces {
static readonly Props = keysOf<BudgetRecord>();
static readonly SecuredProps = keysOf<SecuredProps<BudgetRecord>>();
static readonly Parent = () => import('./budget.dto').then((m) => m.Budget);

@Field(() => Budget)
Expand Down
4 changes: 0 additions & 4 deletions src/components/budget/dto/budget.dto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import {
Calculated,
DbLabel,
IntersectTypes,
Resource,
type ResourceRelationsShape,
SecuredProperty,
type SecuredProps,
Sensitivity,
SensitivityField,
} from '~/common';
Expand All @@ -28,8 +26,6 @@ const Interfaces = IntersectTypes(Resource, ChangesetAware);
implements: Interfaces.members,
})
export class Budget extends Interfaces {
static readonly Props = keysOf<Budget>();
static readonly SecuredProps = keysOf<SecuredProps<Budget>>();
static readonly Relations = (() => ({
records: [BudgetRecord],
})) satisfies ResourceRelationsShape;
Expand Down
4 changes: 0 additions & 4 deletions src/components/ceremony/dto/ceremony.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import {
Calculated,
Resource,
SecuredBoolean,
SecuredDateNullable,
SecuredProperty,
type SecuredProps,
Sensitivity,
SensitivityField,
} from '~/common';
Expand All @@ -20,8 +18,6 @@ import { CeremonyType } from './ceremony-type.enum';
implements: [Resource],
})
export class Ceremony extends Resource {
static readonly Props = keysOf<Ceremony>();
static readonly SecuredProps = keysOf<SecuredProps<Ceremony>>();
static readonly Parent = () =>
import('../../engagement/dto').then((m) => m.IEngagement);

Expand Down
5 changes: 1 addition & 4 deletions src/components/changeset/dto/changeset.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Field, InterfaceType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import { Resource, type SecuredProps } from '~/common';
import { Resource } from '~/common';
import { RegisterResource } from '~/core/resources';

@RegisterResource()
Expand All @@ -9,8 +8,6 @@ import { RegisterResource } from '~/core/resources';
resolveType: (obj: Changeset) => obj.__typename,
})
export class Changeset extends Resource {
static readonly Props: string[] = keysOf<Changeset>();
static readonly SecuredProps: string[] = keysOf<SecuredProps<Changeset>>();
declare __typename: string;

@Field({
Expand Down
5 changes: 0 additions & 5 deletions src/components/comments/dto/comment-thread.dto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import {
type ID,
Resource,
type ResourceRelationsShape,
type SecuredProps,
type SetUnsecuredType,
type UnsecuredDto,
} from '~/common';
Expand All @@ -18,9 +16,6 @@ import { Comment } from './comment.dto';
implements: [Resource],
})
export class CommentThread extends Resource {
static readonly Props = keysOf<CommentThread>();
static readonly SecuredProps: string[] =
keysOf<SecuredProps<CommentThread>>();
static readonly Relations = {
comments: [Comment],
} satisfies ResourceRelationsShape;
Expand Down
11 changes: 1 addition & 10 deletions src/components/comments/dto/comment.dto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { DateTime } from 'luxon';
import { keys as keysOf } from 'ts-transformer-keys';
import {
DateTimeField,
type ID,
Resource,
type SecuredProps,
SecuredRichText,
} from '~/common';
import { DateTimeField, type ID, Resource, SecuredRichText } from '~/common';
import { e } from '~/core/gel';
import { RegisterResource } from '~/core/resources';

Expand All @@ -16,8 +9,6 @@ import { RegisterResource } from '~/core/resources';
implements: [Resource],
})
export class Comment extends Resource {
static readonly Props = keysOf<Comment>();
static readonly SecuredProps: string[] = keysOf<SecuredProps<Comment>>();
static readonly Parent = () =>
import('./comment-thread.dto').then((m) => m.CommentThread);

Expand Down
4 changes: 0 additions & 4 deletions src/components/comments/dto/commentable.dto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { InterfaceType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import {
resolveByTypename,
Resource,
type ResourceRelationsShape,
type SecuredProps,
} from '~/common';
import { e } from '~/core/gel';
import { RegisterResource } from '~/core/resources';
Expand All @@ -17,8 +15,6 @@ import { CommentThread } from './comment-thread.dto';
resolveType: resolveByTypename(Commentable.name),
})
export abstract class Commentable extends Resource {
static readonly Props: string[] = keysOf<Commentable>();
static readonly SecuredProps: string[] = keysOf<SecuredProps<Commentable>>();
static readonly Relations = {
commentThreads: [CommentThread],
} satisfies ResourceRelationsShape;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { ObjectType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import { type SecuredProps } from '~/common';
import { e } from '~/core/gel';
import { type LinkTo, RegisterResource } from '~/core/resources';
import { Notification } from '../../notifications';
Expand All @@ -10,10 +8,6 @@ import { Notification } from '../../notifications';
implements: [Notification],
})
export class CommentViaMentionNotification extends Notification {
static readonly Props = keysOf<CommentViaMentionNotification>();
static readonly SecuredProps =
keysOf<SecuredProps<CommentViaMentionNotification>>();

readonly comment: LinkTo<'Comment'>;
}

Expand Down
8 changes: 0 additions & 8 deletions src/components/engagement/dto/engagement.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Field, InterfaceType, ObjectType } from '@nestjs/graphql';
import { DateTime } from 'luxon';
import { keys as keysOf } from 'ts-transformer-keys';
import { type MergeExclusive } from 'type-fest';
import {
Calculated,
Expand All @@ -17,7 +16,6 @@ import {
SecuredBoolean,
SecuredDateNullable,
SecuredDateTimeNullable,
type SecuredProps,
SecuredRichTextNullable,
SecuredStringNullable,
Sensitivity,
Expand Down Expand Up @@ -71,8 +69,6 @@ const RequiredWhenNotInDev = RequiredWhen(() => Engagement)({
* This should be used for GraphQL but never for TypeScript types.
*/
class Engagement extends Interfaces {
static readonly Props: string[] = keysOf<Engagement>();
static readonly SecuredProps: string[] = keysOf<SecuredProps<Engagement>>();
static readonly Relations = {
...Commentable.Relations,
} satisfies ResourceRelationsShape;
Expand Down Expand Up @@ -167,8 +163,6 @@ export { Engagement as IEngagement, type AnyEngagement as Engagement };
implements: [Engagement],
})
export class LanguageEngagement extends Engagement {
static readonly Props = keysOf<LanguageEngagement>();
static readonly SecuredProps = keysOf<SecuredProps<LanguageEngagement>>();
static readonly Relations = {
...Engagement.Relations,
// why is this singular?
Expand Down Expand Up @@ -218,8 +212,6 @@ export class LanguageEngagement extends Engagement {
implements: [Engagement],
})
export class InternshipEngagement extends Engagement {
static readonly Props = keysOf<InternshipEngagement>();
static readonly SecuredProps = keysOf<SecuredProps<InternshipEngagement>>();
static readonly Parent = () =>
import('../../project/dto').then((m) => m.InternshipProject);

Expand Down
12 changes: 1 addition & 11 deletions src/components/ethno-art/dto/ethno-art.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { ObjectType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import {
DbUnique,
NameField,
Resource,
type SecuredProps,
SecuredString,
} from '~/common';
import { DbUnique, NameField, Resource, SecuredString } from '~/common';
import { e } from '~/core/gel';
import { RegisterResource } from '~/core/resources';
import {
Expand All @@ -26,9 +19,6 @@ declare module '../../product/dto/producible.dto' {
implements: [Producible, Resource],
})
export class EthnoArt extends Producible {
static readonly Props = keysOf<EthnoArt>();
static readonly SecuredProps = keysOf<SecuredProps<EthnoArt>>();

@NameField()
@DbUnique()
readonly name: SecuredString;
Expand Down
5 changes: 0 additions & 5 deletions src/components/field-region/dto/field-region.dto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { ObjectType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import {
DbUnique,
NameField,
Resource,
type Secured,
SecuredProperty,
SecuredPropertyList,
type SecuredProps,
SecuredString,
} from '~/common';
import { e } from '~/core/gel';
Expand All @@ -18,9 +16,6 @@ import { type LinkTo, RegisterResource } from '~/core/resources';
implements: [Resource],
})
export class FieldRegion extends Resource {
static readonly Props = keysOf<FieldRegion>();
static readonly SecuredProps = keysOf<SecuredProps<FieldRegion>>();

@NameField()
@DbUnique()
readonly name: SecuredString;
Expand Down
Loading