@grantjs/schema 1.5.1 → 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.
- package/README.md +32 -15
- package/package.json +10 -4
- package/src/events/event-catalog.ts +12 -15
- package/src/generated/graphql.ts +0 -380
- package/src/generated/resolvers.ts +1197 -4996
- package/src/generated/schema-types.ts +0 -34
- package/src/index.ts +1 -24
- package/src/notifications/notification.types.ts +0 -4
- package/src/operations/role-permissions/role-permissions.graphql +0 -21
- package/src/operations/user-permissions/user-permissions.graphql +0 -21
- package/src/schema/base.graphql +0 -5
- package/src/sdl-contract.test.ts +90 -0
- package/src/test-support/sdl-fixture.ts +79 -0
- package/src/webhooks/webhook.types.ts +0 -8
- package/src/operations/organization-invitations/getOrganizationInvitations.graphql +0 -17
- package/src/schema/account-project-api-keys/inputs/remove-account-project-api-key.graphql +0 -5
- package/src/schema/accounts/types/account-searchable-field.graphql +0 -3
- package/src/schema/me/input/update-my-user-authentication-method.graphql +0 -7
- package/src/schema/organization-members/types/organization-member-searchable-field.graphql +0 -4
- package/src/schema/organization-project-api-keys/inputs/remove-organization-project-api-key.graphql +0 -5
- /package/src/schema/me/{input → inputs}/change-my-password.graphql +0 -0
- /package/src/schema/me/{input → inputs}/create-my-user-authentication-method.graphql +0 -0
- /package/src/schema/me/{input → inputs}/delete-my-accounts.graphql +0 -0
- /package/src/schema/me/{input → inputs}/my-user-sessions.graphql +0 -0
- /package/src/schema/me/{input → inputs}/notification.graphql +0 -0
- /package/src/schema/me/{input → inputs}/update-my-project-membership.graphql +0 -0
- /package/src/schema/me/{input → inputs}/update-my-user.graphql +0 -0
- /package/src/schema/me/{input → inputs}/upload-my-project-membership-picture.graphql +0 -0
- /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
|
|
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
|
-
}
|
package/src/schema/base.graphql
CHANGED
|
@@ -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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|