@grantjs/schema 1.5.2 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +32 -15
  2. package/package.json +10 -4
  3. package/src/events/event-catalog.ts +12 -15
  4. package/src/generated/graphql.ts +0 -380
  5. package/src/generated/resolvers.ts +1197 -4996
  6. package/src/generated/schema-types.ts +0 -34
  7. package/src/index.ts +1 -24
  8. package/src/notifications/notification.types.ts +0 -4
  9. package/src/operations/role-permissions/role-permissions.graphql +0 -21
  10. package/src/operations/user-permissions/user-permissions.graphql +0 -21
  11. package/src/schema/base.graphql +0 -5
  12. package/src/sdl-contract.test.ts +90 -0
  13. package/src/test-support/sdl-fixture.ts +79 -0
  14. package/src/webhooks/webhook.types.ts +0 -8
  15. package/src/operations/organization-invitations/getOrganizationInvitations.graphql +0 -17
  16. package/src/schema/account-project-api-keys/inputs/remove-account-project-api-key.graphql +0 -5
  17. package/src/schema/accounts/types/account-searchable-field.graphql +0 -3
  18. package/src/schema/me/input/update-my-user-authentication-method.graphql +0 -7
  19. package/src/schema/organization-members/types/organization-member-searchable-field.graphql +0 -4
  20. package/src/schema/organization-project-api-keys/inputs/remove-organization-project-api-key.graphql +0 -5
  21. /package/src/schema/me/{input → inputs}/change-my-password.graphql +0 -0
  22. /package/src/schema/me/{input → inputs}/create-my-user-authentication-method.graphql +0 -0
  23. /package/src/schema/me/{input → inputs}/delete-my-accounts.graphql +0 -0
  24. /package/src/schema/me/{input → inputs}/my-user-sessions.graphql +0 -0
  25. /package/src/schema/me/{input → inputs}/notification.graphql +0 -0
  26. /package/src/schema/me/{input → inputs}/update-my-project-membership.graphql +0 -0
  27. /package/src/schema/me/{input → inputs}/update-my-user.graphql +0 -0
  28. /package/src/schema/me/{input → inputs}/upload-my-project-membership-picture.graphql +0 -0
  29. /package/src/schema/me/{input → inputs}/upload-my-user-picture.graphql +0 -0
@@ -108,10 +108,6 @@ export type AccountRole = Auditable & {
108
108
  updatedAt: Scalars['Date']['output'];
109
109
  };
110
110
 
111
- export enum AccountSearchableField {
112
- Type = 'type',
113
- }
114
-
115
111
  export type AccountSortInput = {
116
112
  field: AccountSortableField;
117
113
  order: SortOrder;
@@ -486,11 +482,6 @@ export type ChangeMyPasswordResult = {
486
482
  success: Scalars['Boolean']['output'];
487
483
  };
488
484
 
489
- export type Creatable = {
490
- createdAt: Scalars['String']['output'];
491
- updatedAt: Scalars['String']['output'];
492
- };
493
-
494
485
  export type CreateAccountInput = {
495
486
  ownerId: Scalars['String']['input'];
496
487
  provider: UserAuthenticationMethodProvider;
@@ -1626,11 +1617,6 @@ export type OrganizationMemberPage = {
1626
1617
  totalCount: Scalars['Int']['output'];
1627
1618
  };
1628
1619
 
1629
- export enum OrganizationMemberSearchableField {
1630
- Email = 'email',
1631
- Name = 'name',
1632
- }
1633
-
1634
1620
  export type OrganizationMemberSortInput = {
1635
1621
  field: OrganizationMemberSortableField;
1636
1622
  order: SortOrder;
@@ -2594,12 +2580,6 @@ export type RegisterInput = {
2594
2580
  type: AccountType;
2595
2581
  };
2596
2582
 
2597
- export type RemoveAccountProjectApiKeyInput = {
2598
- accountId: Scalars['ID']['input'];
2599
- apiKeyId: Scalars['ID']['input'];
2600
- projectId: Scalars['ID']['input'];
2601
- };
2602
-
2603
2583
  export type RemoveAccountProjectInput = {
2604
2584
  accountId: Scalars['ID']['input'];
2605
2585
  projectId: Scalars['ID']['input'];
@@ -2649,12 +2629,6 @@ export type RemoveOrganizationPermissionInput = {
2649
2629
  permissionId: Scalars['ID']['input'];
2650
2630
  };
2651
2631
 
2652
- export type RemoveOrganizationProjectApiKeyInput = {
2653
- apiKeyId: Scalars['ID']['input'];
2654
- organizationId: Scalars['ID']['input'];
2655
- projectId: Scalars['ID']['input'];
2656
- };
2657
-
2658
2632
  export type RemoveOrganizationProjectInput = {
2659
2633
  organizationId: Scalars['ID']['input'];
2660
2634
  projectId: Scalars['ID']['input'];
@@ -3274,14 +3248,6 @@ export type UpdateMyProjectMembershipInput = {
3274
3248
  projectId: Scalars['ID']['input'];
3275
3249
  };
3276
3250
 
3277
- export type UpdateMyUserAuthenticationMethodInput = {
3278
- isPrimary?: InputMaybe<Scalars['Boolean']['input']>;
3279
- isVerified?: InputMaybe<Scalars['Boolean']['input']>;
3280
- provider?: InputMaybe<UserAuthenticationMethodProvider>;
3281
- providerData?: InputMaybe<Scalars['JSON']['input']>;
3282
- providerId?: InputMaybe<Scalars['String']['input']>;
3283
- };
3284
-
3285
3251
  export type UpdateMyUserInput = {
3286
3252
  metadata?: InputMaybe<Scalars['JSON']['input']>;
3287
3253
  name?: InputMaybe<Scalars['String']['input']>;
package/src/index.ts CHANGED
@@ -1,30 +1,7 @@
1
1
  export * from './cdm/cdm-export-sections';
2
2
  export * from './events';
3
3
  export * from './generated/graphql';
4
- export type {
5
- AccountResolvers,
6
- GroupResolvers,
7
- MutationResolvers,
8
- OrganizationInvitationResolvers,
9
- OrganizationResolvers,
10
- PermissionResolvers,
11
- ProjectAppResolvers,
12
- ProjectResolvers,
13
- QueryResolvers,
14
- RequireFields,
15
- Resolver,
16
- Resolvers,
17
- ResolversObject,
18
- ResolversParentTypes,
19
- ResolversTypes,
20
- ResolverTypeWrapper,
21
- ResourceResolvers,
22
- RolePermissionResolvers,
23
- RoleResolvers,
24
- TagResolvers,
25
- UserPermissionResolvers,
26
- UserResolvers,
27
- } from './generated/resolvers';
4
+ export * from './generated/resolvers';
28
5
  export * from './generated/schema-types';
29
6
  export * from './notifications/notification.types';
30
7
  export type {
@@ -2,10 +2,6 @@ import type { EventCategory } from '../events/event-catalog';
2
2
 
3
3
  export const NOTIFICATION_CHANNELS = ['in_app', 'email'] as const;
4
4
 
5
- export const NOTIFICATION_STATUSES = ['pending', 'delivered', 'failed', 'dead'] as const;
6
-
7
- export const NOTIFICATION_PREFERENCE_SOURCES = ['user', 'org_enforced'] as const;
8
-
9
5
  export interface ListNotificationsInput {
10
6
  unreadOnly?: boolean;
11
7
  page?: number;
@@ -16,24 +16,3 @@ mutation RevokeRolePermission($input: RevokeRolePermissionInput!) {
16
16
  deletedAt
17
17
  }
18
18
  }
19
-
20
- query GetRoleDirectPermissions($scope: Scope!, $roleId: ID!) {
21
- roles(scope: $scope, ids: [$roleId], limit: 1) {
22
- roles {
23
- id
24
- permissionCount
25
- rolePermissions {
26
- id
27
- permissionId
28
- permission(scope: $scope) {
29
- id
30
- name
31
- action
32
- resource {
33
- slug
34
- }
35
- }
36
- }
37
- }
38
- }
39
- }
@@ -16,24 +16,3 @@ mutation RevokeUserPermission($input: RevokeUserPermissionInput!) {
16
16
  deletedAt
17
17
  }
18
18
  }
19
-
20
- query GetUserDirectPermissions($scope: Scope!, $userId: ID!) {
21
- users(scope: $scope, ids: [$userId], limit: 1) {
22
- users {
23
- id
24
- permissionCount
25
- userPermissions {
26
- id
27
- permissionId
28
- permission(scope: $scope) {
29
- id
30
- name
31
- action
32
- resource {
33
- slug
34
- }
35
- }
36
- }
37
- }
38
- }
39
- }
@@ -15,11 +15,6 @@ interface Auditable {
15
15
  deletedAt: Date
16
16
  }
17
17
 
18
- interface Creatable {
19
- createdAt: String!
20
- updatedAt: String!
21
- }
22
-
23
18
  interface PaginatedResults {
24
19
  totalCount: Int!
25
20
  hasNextPage: Boolean!
@@ -0,0 +1,90 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { relative } from 'node:path';
3
+
4
+ import { parse, validate } from 'graphql';
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import {
8
+ buildMergedSchema,
9
+ declaredTypes,
10
+ operationFiles,
11
+ reachableTypeNames,
12
+ sdlFiles,
13
+ } from './test-support/sdl-fixture';
14
+
15
+ /**
16
+ * The schema is the package's real deliverable, and until now nothing executed it:
17
+ * `apps/api` was the first thing to find out whether the SDL builds, at boot.
18
+ * These are the structural assertions that belong to the package that owns the SDL.
19
+ */
20
+ describe('merged SDL', () => {
21
+ const schema = buildMergedSchema();
22
+
23
+ it('builds from every file under src/schema', () => {
24
+ expect(sdlFiles().length).toBeGreaterThan(0);
25
+ expect(schema.getQueryType()).toBeDefined();
26
+ expect(schema.getMutationType()).toBeDefined();
27
+ });
28
+
29
+ it('exposes the root fields the API serves', () => {
30
+ // Guards against a merge that silently drops `extend type Query|Mutation` blocks:
31
+ // the schema would still build, with a near-empty root.
32
+ expect(Object.keys(schema.getQueryType()!.getFields()).length).toBeGreaterThanOrEqual(32);
33
+ expect(Object.keys(schema.getMutationType()!.getFields()).length).toBeGreaterThanOrEqual(84);
34
+ });
35
+ });
36
+
37
+ describe('operation documents', () => {
38
+ const schema = buildMergedSchema();
39
+ const files = operationFiles();
40
+
41
+ // Pinned, not a floor: pass 5 slice 5 removed one file and two query operations
42
+ // that were superseded by list queries, and a floor would have absorbed that
43
+ // silently. 116 before that slice.
44
+ it('finds every document under src/operations', () => {
45
+ expect(files).toHaveLength(115);
46
+ });
47
+
48
+ it.each(files.map((f) => [relative(process.cwd(), f), f]))('%s validates', (_label, file) => {
49
+ const errors = validate(schema, parse(readFileSync(file, 'utf8')));
50
+ expect(errors.map((e) => e.message)).toEqual([]);
51
+ });
52
+ });
53
+
54
+ describe('graph reachability', () => {
55
+ const schema = buildMergedSchema();
56
+
57
+ /**
58
+ * 175 of 382 declared types cannot be reached from Query or Mutation. They are
59
+ * not dead: most are consumed as generated TypeScript by apps/api, and the
60
+ * *SearchableField enums are runtime search configuration. But every one of them
61
+ * ships in the schema `makeExecutableSchema` serves.
62
+ *
63
+ * This number is the input to the Tier 3 decision in
64
+ * docs/contributing/code-quality/schema.md — pinned so it moves visibly.
65
+ * Deleting unreachable SDL lowers it; adding a type with no query or mutation
66
+ * raises it. Either way, update it deliberately.
67
+ *
68
+ * Was 181 of 388 before pass 5 slice 4 removed six declarations that were
69
+ * unreferenced in SDL and in all TypeScript. The reachable count did not change
70
+ * (207 before and after), which is what makes those six safe to have deleted.
71
+ */
72
+ const UNREACHABLE_FROM_ROOT = 175;
73
+
74
+ it('pins the count of types unreachable from Query/Mutation', () => {
75
+ const reachable = reachableTypeNames(schema);
76
+ const unreachable = declaredTypes(schema).filter((t) => !reachable.has(t.name));
77
+
78
+ expect(unreachable).toHaveLength(UNREACHABLE_FROM_ROOT);
79
+ });
80
+
81
+ it('keeps every operation document inside the reachable set', () => {
82
+ // A document can only select reachable types, so this is a consistency check on
83
+ // the traversal itself: if it under-reports, the validation suite above would
84
+ // still pass while this pins a wrong number.
85
+ const reachable = reachableTypeNames(schema);
86
+ expect(reachable.has('Query')).toBe(true);
87
+ expect(reachable.has('Mutation')).toBe(true);
88
+ expect(reachable.size).toBe(declaredTypes(schema).length - UNREACHABLE_FROM_ROOT);
89
+ });
90
+ });
@@ -0,0 +1,79 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import {
6
+ buildSchema,
7
+ getNamedType,
8
+ type GraphQLNamedType,
9
+ type GraphQLSchema,
10
+ isInputObjectType,
11
+ isInterfaceType,
12
+ isObjectType,
13
+ isUnionType,
14
+ } from 'graphql';
15
+
16
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
17
+
18
+ function graphqlFilesUnder(dir: string): string[] {
19
+ return readdirSync(dir)
20
+ .flatMap((entry) => {
21
+ const path = join(dir, entry);
22
+ if (statSync(path).isDirectory()) return graphqlFilesUnder(path);
23
+ return path.endsWith('.graphql') ? [path] : [];
24
+ })
25
+ .sort();
26
+ }
27
+
28
+ export const sdlFiles = (): string[] => graphqlFilesUnder(join(packageRoot, 'src', 'schema'));
29
+ export const operationFiles = (): string[] =>
30
+ graphqlFilesUnder(join(packageRoot, 'src', 'operations'));
31
+
32
+ export const readAll = (files: string[]): string =>
33
+ files.map((f) => readFileSync(f, 'utf8')).join('\n');
34
+
35
+ /**
36
+ * Builds the merged schema from every file under `src/schema`, which is the same
37
+ * set `apps/api` feeds to `makeExecutableSchema` via `loadFilesSync` — see
38
+ * apps/api/src/graphql/resolvers/index.ts. Concatenation is equivalent here because
39
+ * the schema is one directory of SDL with no imports between files.
40
+ */
41
+ export function buildMergedSchema(): GraphQLSchema {
42
+ return buildSchema(readAll(sdlFiles()));
43
+ }
44
+
45
+ /** Named types excluding GraphQL's introspection meta-types. */
46
+ export function declaredTypes(schema: GraphQLSchema): GraphQLNamedType[] {
47
+ return Object.values(schema.getTypeMap()).filter((t) => !t.name.startsWith('__'));
48
+ }
49
+
50
+ /**
51
+ * Types reachable by traversing fields, arguments, interfaces, and union members
52
+ * from `Query` and `Mutation`. Anything outside this set is in the served schema
53
+ * and visible to introspection, but no operation can reach it.
54
+ */
55
+ export function reachableTypeNames(schema: GraphQLSchema): Set<string> {
56
+ const seen = new Set<string>();
57
+
58
+ const visit = (type: unknown): void => {
59
+ const named = getNamedType(type as never) as GraphQLNamedType | undefined;
60
+ if (!named || seen.has(named.name) || named.name.startsWith('__')) return;
61
+ seen.add(named.name);
62
+
63
+ if (isObjectType(named) || isInterfaceType(named)) {
64
+ for (const field of Object.values(named.getFields())) {
65
+ visit(field.type);
66
+ for (const arg of field.args) visit(arg.type);
67
+ }
68
+ for (const iface of named.getInterfaces()) visit(iface);
69
+ } else if (isInputObjectType(named)) {
70
+ for (const field of Object.values(named.getFields())) visit(field.type);
71
+ } else if (isUnionType(named)) {
72
+ for (const member of named.getTypes()) visit(member);
73
+ }
74
+ };
75
+
76
+ visit(schema.getQueryType());
77
+ visit(schema.getMutationType());
78
+ return seen;
79
+ }
@@ -1,13 +1,5 @@
1
1
  import type { EventType } from '../events/event-catalog';
2
2
 
3
- export const WEBHOOK_DELIVERY_STATUSES = [
4
- 'pending',
5
- 'running',
6
- 'delivered',
7
- 'failed',
8
- 'dead',
9
- ] as const;
10
-
11
3
  /** Service-layer create payload (scope is passed separately). */
12
4
  export interface CreateWebhookSubscriptionInput {
13
5
  url: string;
@@ -1,17 +0,0 @@
1
- query GetOrganizationInvitations($scope: Scope!, $status: OrganizationInvitationStatus) {
2
- organizationInvitations(scope: $scope, status: $status) {
3
- invitations {
4
- id
5
- organizationId
6
- email
7
- roleId
8
- status
9
- expiresAt
10
- invitedBy
11
- invitedAt
12
- createdAt
13
- }
14
- totalCount
15
- hasNextPage
16
- }
17
- }
@@ -1,5 +0,0 @@
1
- input RemoveAccountProjectApiKeyInput {
2
- accountId: ID!
3
- projectId: ID!
4
- apiKeyId: ID!
5
- }
@@ -1,3 +0,0 @@
1
- enum AccountSearchableField {
2
- type
3
- }
@@ -1,7 +0,0 @@
1
- input UpdateMyUserAuthenticationMethodInput {
2
- providerId: String
3
- provider: UserAuthenticationMethodProvider
4
- providerData: JSON
5
- isVerified: Boolean
6
- isPrimary: Boolean
7
- }
@@ -1,4 +0,0 @@
1
- enum OrganizationMemberSearchableField {
2
- name
3
- email
4
- }
@@ -1,5 +0,0 @@
1
- input RemoveOrganizationProjectApiKeyInput {
2
- organizationId: ID!
3
- projectId: ID!
4
- apiKeyId: ID!
5
- }