Make Member.id unique (#4794)

This commit is contained in:
Kamil Kisiela 2024-05-22 09:55:27 +02:00 committed by GitHub
parent c4dcb47534
commit 7c476f941d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 158 additions and 378 deletions

View file

@ -78,14 +78,9 @@ module.exports = {
},
{
files: ['packages/web/app/**/*.graphql'],
plugins: ['@graphql-eslint', 'hive'],
plugins: ['@graphql-eslint'],
rules: {
'@graphql-eslint/require-id-when-available': 'error',
// Require temporaryFixId field when available.
// We need it to be able to cache Member objects in the frontend properly.
// Member.id is not unique, so we need to use Member.temporaryFixId instead.
// Once we have a better solution, we can remove this rule.
'hive/graphql-require-selection': ['error'],
},
},
{

View file

@ -194,6 +194,13 @@ const config: CodegenConfig = {
documents: ['./integration-tests/(testkit|tests)/**/*.ts'],
preset: 'client',
plugins: [],
config: {
scalars: {
DateTime: 'string',
Date: 'string',
SafeInt: 'number',
},
},
},
'./schema.graphql': {
plugins: ['schema-ast'],

View file

@ -5,7 +5,8 @@
"private": true,
"scripts": {
"prepare:env": "cd ../ && pnpm build:libraries && pnpm build:services",
"test:integration": "vitest"
"test:integration": "vitest",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@apollo/gateway": "2.7.8",

View file

@ -173,6 +173,9 @@ export function joinOrganization(code: string, authToken: string) {
cleanId
me {
id
user {
id
}
organizationAccessScopes
projectAccessScopes
targetAccessScopes

View file

@ -1,4 +1,4 @@
import { ProjectType, RegistryModel, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, RegistryModel, TargetAccessScope } from 'testkit/gql/graphql';
import { initSeed } from './seed';
export async function prepareProject(

View file

@ -1,4 +1,4 @@
import { RuleInstanceSeverityLevel, SchemaPolicyInput } from '@app/gql/graphql';
import { RuleInstanceSeverityLevel, SchemaPolicyInput } from 'testkit/gql/graphql';
import { graphql } from './gql';
export const TargetCalculatedPolicy = graphql(`

View file

@ -1,3 +1,4 @@
import type { UUID } from 'node:crypto';
import { humanId } from 'human-id';
import { createPool, sql } from 'slonik';
import {
@ -7,7 +8,7 @@ import {
RegistryModel,
SchemaPolicyInput,
TargetAccessScope,
} from '@app/gql/graphql';
} from 'testkit/gql/graphql';
import { authenticate, userEmail } from './auth';
import {
CreateCollectionMutation,
@ -720,7 +721,7 @@ export function initSeed() {
async assignMemberRole(
input: {
roleId: string;
memberId: string;
userId: string;
},
options: { useMemberToken?: boolean } = {
useMemberToken: false,
@ -729,7 +730,7 @@ export function initSeed() {
const memberRoleAssignmentResult = await assignMemberRole(
{
organization: organization.cleanId,
member: input.memberId,
user: input.userId,
role: input.roleId,
},
options.useMemberToken ? memberToken : ownerToken,

View file

@ -1,9 +1,9 @@
import crypto from 'node:crypto';
import bcrypt from 'bcryptjs';
import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { ApolloGateway } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
import {
DeleteObjectsCommand,
GetObjectCommand,

View file

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { initSeed } from '../../../testkit/seed';
describe('Document Collections', () => {

View file

@ -1,6 +1,6 @@
import { formatISO } from 'date-fns/formatISO';
import { subHours } from 'date-fns/subHours';
import { ProjectType } from '@app/gql/graphql';
import { ProjectType } from 'testkit/gql/graphql';
import { waitFor } from '../../testkit/flow';
import { initSeed } from '../../testkit/seed';

View file

@ -3,7 +3,7 @@ import {
ProjectAccessScope,
ProjectType,
TargetAccessScope,
} from '@app/gql/graphql';
} from 'testkit/gql/graphql';
import { waitFor } from '../../../testkit/flow';
import { initSeed } from '../../../testkit/seed';

View file

@ -1,4 +1,8 @@
import { OrganizationAccessScope, ProjectAccessScope, TargetAccessScope } from '@app/gql/graphql';
import {
OrganizationAccessScope,
ProjectAccessScope,
TargetAccessScope,
} from 'testkit/gql/graphql';
import { history } from '../../../testkit/emails';
import { initSeed } from '../../../testkit/seed';
@ -56,7 +60,7 @@ test.concurrent(
});
await assignMemberRole({
roleId: membersManagerRole.id,
memberId: member.id,
userId: member.user.id,
});
await expect(
@ -91,7 +95,7 @@ test.concurrent(
});
await assignMemberRole({
roleId: membersManagerRole.id,
memberId: member.id,
userId: member.user.id,
});
const adminRoleId = organization.memberRoles.find(r => r.name === 'Admin')?.id;
@ -104,7 +108,7 @@ test.concurrent(
assignMemberRole(
{
roleId: adminRoleId,
memberId: viewerRoleMember.id,
userId: viewerRoleMember.user.id,
},
{
useMemberToken: true,
@ -166,11 +170,11 @@ test.concurrent('cannot downgrade a member when assigning a new role', async ()
});
await assignMemberRole({
roleId: managerRole.id,
memberId: member.id,
userId: member.user.id,
});
await assignMemberRole({
roleId: originalRole.id,
memberId: viewerRoleMember.id,
userId: viewerRoleMember.user.id,
});
// non-admin member cannot downgrade another member
@ -178,7 +182,7 @@ test.concurrent('cannot downgrade a member when assigning a new role', async ()
assignMemberRole(
{
roleId: roleWithLessAccess.id,
memberId: viewerRoleMember.id,
userId: viewerRoleMember.user.id,
},
{
useMemberToken: true,
@ -188,7 +192,7 @@ test.concurrent('cannot downgrade a member when assigning a new role', async ()
// admin can downgrade another member
await assignMemberRole({
roleId: roleWithLessAccess.id,
memberId: viewerRoleMember.id,
userId: viewerRoleMember.user.id,
});
});
@ -219,11 +223,11 @@ test.concurrent('cannot downgrade a member when modifying a role', async () => {
});
await assignMemberRole({
roleId: managerRole.id,
memberId: member.id,
userId: member.user.id,
});
await assignMemberRole({
roleId: roleToBeUpdated.id,
memberId: viewerRoleMember.id,
userId: viewerRoleMember.user.id,
});
// non-admin member cannot downgrade another member
@ -267,11 +271,11 @@ test.concurrent('cannot delete a role with members', async () => {
});
await assignMemberRole({
roleId: membersManagerRole.id,
memberId: member.id,
userId: member.user.id,
});
await assignMemberRole({
roleId: readOnlyRole.id,
memberId: viewerRoleMember.id,
userId: viewerRoleMember.user.id,
});
// delete the role as the owner
@ -316,7 +320,7 @@ test.concurrent('cannot invite a member with more access than the inviter', asyn
// give the inviting member a role with enough access to invite other members
await assignMemberRole({
roleId: membersManagerRole.id,
memberId: invitingMember.id,
userId: invitingMember.user.id,
});
const inviteEmail = seed.generateEmail();

View file

@ -1,4 +1,8 @@
import { OrganizationAccessScope, ProjectAccessScope, TargetAccessScope } from '@app/gql/graphql';
import {
OrganizationAccessScope,
ProjectAccessScope,
TargetAccessScope,
} from 'testkit/gql/graphql';
import {
answerOrganizationTransferRequest,
getOrganizationTransferRequest,
@ -32,7 +36,7 @@ test.concurrent('owner should be able to request the ownership transfer to a mem
const transferRequestResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
@ -49,7 +53,7 @@ test.concurrent('non-owner should not be able to request the ownership transfer'
const errors = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: orgMembers.find(u => u.user.email === ownerEmail)!.id,
user: orgMembers.find(u => u.user.email === ownerEmail)!.user.id,
},
memberToken,
).then(r => r.expectGraphQLErrors());
@ -68,7 +72,7 @@ test.concurrent(
const transferRequestResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
memberToken,
).then(r => r.expectNoGraphQLErrors());
@ -85,7 +89,7 @@ test.concurrent('non-member should not be able to access the transfer request',
const requestTransferResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
@ -119,7 +123,7 @@ test.concurrent('non-recipient should not be able to access the transfer request
const requestTransferResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
@ -148,7 +152,7 @@ test.concurrent('recipient should be able to access the transfer request', async
const requestTransferResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
@ -178,7 +182,7 @@ test.concurrent('recipient should be able to answer the ownership transfer', asy
const requestTransferResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
@ -210,7 +214,7 @@ test.concurrent('non-member should not be able to answer the ownership transfer'
const requestTransferResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
@ -241,7 +245,7 @@ test.concurrent('owner should not be able to answer the ownership transfer', asy
const requestTransferResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
@ -272,7 +276,7 @@ test.concurrent('non-member should not be able to answer the ownership transfer'
const requestTransferResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
@ -306,7 +310,7 @@ test.concurrent(
const requestTransferResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
@ -355,7 +359,7 @@ test.concurrent(
const requestTransferResult = await requestOrganizationTransfer(
{
organization: organization.cleanId,
user: member.id,
user: member.user.id,
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());

View file

@ -1,4 +1,4 @@
import { ProjectType } from '@app/gql/graphql';
import { ProjectType } from 'testkit/gql/graphql';
import { graphql } from '../../../testkit/gql';
import { execute } from '../../../testkit/graphql';
import { initSeed } from '../../../testkit/seed';
@ -32,7 +32,7 @@ describe('Policy Access', () => {
const { member, memberToken, assignMemberRole } = await inviteAndJoinMember();
await assignMemberRole({
roleId: adminRole.id,
memberId: member.id,
userId: member.user.id,
});
const result = await execute({
@ -100,7 +100,7 @@ describe('Policy Access', () => {
const { member, memberToken, assignMemberRole } = await inviteAndJoinMember();
await assignMemberRole({
roleId: adminRole.id,
memberId: member.id,
userId: member.user.id,
});
const result = await execute({
@ -166,7 +166,7 @@ describe('Policy Access', () => {
const { member, memberToken, assignMemberRole } = await inviteAndJoinMember();
await assignMemberRole({
roleId: adminRole.id,
memberId: member.id,
userId: member.user.id,
});
const result = await execute({

View file

@ -1,6 +1,6 @@
import stripAnsi from 'strip-ansi';
import { ProjectType, RuleInstanceSeverityLevel, SchemaPolicyInput } from 'testkit/gql/graphql';
import { prepareProject } from 'testkit/registry-models';
import { ProjectType, RuleInstanceSeverityLevel, SchemaPolicyInput } from '@app/gql/graphql';
import { createCLI } from '../../../testkit/cli';
export const createPolicy = (level: RuleInstanceSeverityLevel): SchemaPolicyInput => ({

View file

@ -1,4 +1,4 @@
import { ProjectType } from '@app/gql/graphql';
import { ProjectType } from 'testkit/gql/graphql';
import { execute } from '../../../testkit/graphql';
import {
DESCRIPTION_RULE,

View file

@ -1,4 +1,4 @@
import { ProjectType } from '@app/gql/graphql';
import { ProjectType } from 'testkit/gql/graphql';
import { renameProject } from '../../../testkit/flow';
import { initSeed } from '../../../testkit/seed';

View file

@ -3,7 +3,7 @@ import {
ProjectAccessScope,
ProjectType,
TargetAccessScope,
} from '@app/gql/graphql';
} from 'testkit/gql/graphql';
import * as emails from '../../../testkit/emails';
import { updateOrgRateLimit, waitFor } from '../../../testkit/flow';
import { initSeed } from '../../../testkit/seed';

View file

@ -1,4 +1,4 @@
import { ProjectType, RuleInstanceSeverityLevel, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, RuleInstanceSeverityLevel, TargetAccessScope } from 'testkit/gql/graphql';
import { graphql } from '../../../testkit/gql';
import { execute } from '../../../testkit/graphql';
import { initSeed } from '../../../testkit/seed';

View file

@ -1,4 +1,4 @@
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { initSeed } from '../../../testkit/seed';
describe.each`

View file

@ -1,4 +1,4 @@
import { ProjectAccessScope, ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectAccessScope, ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { enableExternalSchemaComposition } from '../../../testkit/flow';
import { initSeed } from '../../../testkit/seed';
import { generateUnique } from '../../../testkit/utils';

View file

@ -1,4 +1,4 @@
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { graphql } from '../../../testkit/gql';
import { execute } from '../../../testkit/graphql';
import { initSeed } from '../../../testkit/seed';

View file

@ -1,4 +1,4 @@
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { GetObjectCommand, NoSuchKey, S3Client } from '@aws-sdk/client-s3';
import { graphql } from '../../../testkit/gql';
import { execute } from '../../../testkit/graphql';

View file

@ -1,8 +1,8 @@
import 'reflect-metadata';
import { parse, print } from 'graphql';
import { enableExternalSchemaComposition } from 'testkit/flow';
import { ProjectAccessScope, ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { initSeed } from 'testkit/seed';
import { ProjectAccessScope, ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { createStorage } from '@hive/storage';
import { sortSDL } from '@theguild/federation-composition';
@ -131,7 +131,7 @@ test.concurrent(
test.concurrent(
'the changes and schema sdl is persisted in the database when the super schema schema is composable',
async ({ expect }) => {
let storage: Awaited<ReturnType<typeof createStorage>>;
let storage: Awaited<ReturnType<typeof createStorage>> | undefined = undefined;
try {
storage = await createStorage(connectionString(), 1);
@ -228,7 +228,7 @@ test.concurrent(
test.concurrent(
'composition error is persisted in the database when the super schema schema is not composable',
async ({ expect }) => {
let storage: Awaited<ReturnType<typeof createStorage>>;
let storage: Awaited<ReturnType<typeof createStorage>> | undefined = undefined;
try {
storage = await createStorage(connectionString(), 1);

View file

@ -1,4 +1,4 @@
import { ProjectAccessScope, ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectAccessScope, ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { history, serviceName, servicePort } from '../../../testkit/external-composition';
import { enableExternalSchemaComposition } from '../../../testkit/flow';
import { initSeed } from '../../../testkit/seed';

View file

@ -1,9 +1,9 @@
import 'reflect-metadata';
import { createPool, sql } from 'slonik';
import { graphql } from 'testkit/gql';
import { execute } from 'testkit/graphql';
/* eslint-disable no-process-env */
import { ProjectAccessScope, ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectAccessScope, ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { execute } from 'testkit/graphql';
// eslint-disable-next-line import/no-extraneous-dependencies
import { createStorage } from '@hive/storage';
import {
@ -691,6 +691,10 @@ describe('schema publishing changes are persisted', () => {
versionId: latestVersion.id,
});
if (!Array.isArray(changes)) {
throw new Error('Expected changes to be an array');
}
expect(changes[0]['meta']).toEqual(args.equalsObject['meta']);
expect(changes[0]['type']).toEqual(args.equalsObject['type']);
});
@ -2778,6 +2782,7 @@ test('Target.schemaVersion: result is read from the database', async () => {
const latestVersion = await storage.getLatestVersion({
target: target.id,
project: project.id,
organization: organization.id,
});
const result = await execute({

View file

@ -1,7 +1,7 @@
import { graphql } from 'testkit/gql';
import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { execute } from 'testkit/graphql';
import { initSeed } from 'testkit/seed';
import { graphql } from '@app/gql';
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
const CreateContractMutation = graphql(`
mutation CreateContractMutation($input: CreateContractInput!) {

View file

@ -1,4 +1,4 @@
import { ProjectType } from '@app/gql/graphql';
import { ProjectType } from 'testkit/gql/graphql';
import { renameTarget } from '../../../testkit/flow';
import { initSeed } from '../../../testkit/seed';

View file

@ -1,4 +1,4 @@
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { initSeed } from '../../../testkit/seed';
test.concurrent(

View file

@ -2,15 +2,15 @@ import { formatISO } from 'date-fns/formatISO';
import { subHours } from 'date-fns/subHours';
import { buildASTSchema, parse, print } from 'graphql';
import { createLogger } from 'graphql-yoga';
import { execute } from 'testkit/graphql';
import { getServiceHost } from 'testkit/utils';
import { graphql } from '@app/gql';
import { graphql } from 'testkit/gql';
import {
OrganizationAccessScope,
ProjectAccessScope,
ProjectType,
TargetAccessScope,
} from '@app/gql/graphql';
} from 'testkit/gql/graphql';
import { execute } from 'testkit/graphql';
import { getServiceHost } from 'testkit/utils';
// eslint-disable-next-line hive/enforce-deps-in-dev
import { normalizeOperation } from '@graphql-hive/core';
import { createHive } from '../../../../packages/libraries/core/src';

View file

@ -1,5 +1,5 @@
import { readTokenInfo, waitFor } from 'testkit/flow';
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { initSeed } from '../../testkit/seed';
test.concurrent('deleting a token should clear the cache', async () => {

View file

@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { ProjectType } from '@app/gql/graphql';
import { ProjectType } from 'testkit/gql/graphql';
import { createCLI } from '../../testkit/cli';
import { initSeed } from '../../testkit/seed';

View file

@ -1,6 +1,6 @@
/* eslint-disable no-process-env */
import { createHash } from 'node:crypto';
import { ProjectType } from '@app/gql/graphql';
import { ProjectType } from 'testkit/gql/graphql';
import { createCLI, schemaCheck, schemaPublish } from '../../testkit/cli';
import { initSeed } from '../../testkit/seed';

View file

@ -1,4 +1,4 @@
import { ProjectType, RegistryModel } from '@app/gql/graphql';
import { ProjectType, RegistryModel } from 'testkit/gql/graphql';
import { createCLI, schemaPublish } from '../../testkit/cli';
import { prepareProject } from '../../testkit/registry-models';
import { initSeed } from '../../testkit/seed';

View file

@ -1,4 +1,4 @@
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql';
import { normalizeCliOutput } from '../../../scripts/serializers/cli-output';
import { createCLI, schemaPublish } from '../../testkit/cli';
import { prepareProject } from '../../testkit/registry-models';

View file

@ -1,4 +1,4 @@
import { ProjectType, RegistryModel, TargetAccessScope } from '@app/gql/graphql';
import { ProjectType, RegistryModel, TargetAccessScope } from 'testkit/gql/graphql';
import { createCLI } from '../../testkit/cli';
import { prepareProject } from '../../testkit/registry-models';
import { initSeed } from '../../testkit/seed';

View file

@ -1,4 +1,4 @@
import { ProjectType } from '@app/gql/graphql';
import { ProjectType } from 'testkit/gql/graphql';
import { normalizeCliOutput } from '../../../scripts/serializers/cli-output';
import { createCLI } from '../../testkit/cli';
import { prepareProject } from '../../testkit/registry-models';

View file

@ -1,4 +1,4 @@
import { ProjectType, RegistryModel } from '@app/gql/graphql';
import { ProjectType, RegistryModel } from 'testkit/gql/graphql';
import { createCLI } from '../../testkit/cli';
import { prepareProject } from '../../testkit/registry-models';

View file

@ -1,4 +1,4 @@
import { ProjectType } from '@app/gql/graphql';
import { ProjectType } from 'testkit/gql/graphql';
import { normalizeCliOutput } from '../../../scripts/serializers/cli-output';
import { createCLI } from '../../testkit/cli';
import { prepareProject } from '../../testkit/registry-models';

View file

@ -1,18 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"baseUrl": ".",
"esModuleInterop": true,
"paths": {
"@hive/service-common": ["../packages/services/service-common/src/index.ts"],
"@hive/server": ["../packages/services/server/src/api.ts"],
"@hive/storage": ["../packages/services/storage/src/index.ts"],
"@hive/rate-limit": ["../packages/services/rate-limit/src/api.ts"],
"@app/gql/graphql": ["./testkit/gql/graphql.ts"],
"@app/gql": ["./testkit/gql/index.ts"],
"@hive/schema": ["../packages/services/schema/src/api.ts"]
}
"esModuleInterop": true
},
"include": ["testkit", "tests", "./expect.ts"]
"include": ["./testkit", "./tests", "./expect.ts"]
}

View file

@ -4,8 +4,8 @@ export default defineConfig({
test: {
globals: true,
alias: {
'@app/gql/graphql': new URL('./testkit/gql/graphql.ts', import.meta.url).pathname,
'@app/gql': new URL('./testkit/gql/index.ts', import.meta.url).pathname,
'testkit/gql/graphql': new URL('./testkit/gql/graphql.ts', import.meta.url).pathname,
'testkit/gql': new URL('./testkit/gql/index.ts', import.meta.url).pathname,
'@hive/service-common': new URL(
'../packages/services/service-common/src/index.ts',
import.meta.url,

View file

@ -1 +1 @@
export const version = '0.32.0';
export const version = '0.33.0';

View file

@ -1 +1 @@
export const version = '0.2.4';
export const version = '0.3.0';

View file

@ -1 +1 @@
export const version = '0.32.0';
export const version = '0.33.0';

View file

@ -1 +1 @@
export const version = '0.32.0';
export const version = '0.33.0';

View file

@ -59,7 +59,6 @@ export default gql`
type Member {
id: ID!
temporaryFixId: ID!
user: User!
isOwner: Boolean!
organizationAccessScopes: [OrganizationAccessScope!]!

View file

@ -1,4 +1,3 @@
import Dataloader from 'dataloader';
import DataLoader from 'dataloader';
import { forwardRef, Inject, Injectable, Scope } from 'graphql-modules';
import { Token } from '../../../shared/entities';
@ -45,8 +44,8 @@ export function isOrganizationScope(scope: any): scope is OrganizationAccessScop
})
export class OrganizationAccess {
private logger: Logger;
private userAccess: Dataloader<OrganizationUserAccessSelector, boolean, string>;
private tokenAccess: Dataloader<OrganizationTokenAccessSelector, boolean, string>;
private userAccess: DataLoader<OrganizationUserAccessSelector, boolean, string>;
private tokenAccess: DataLoader<OrganizationTokenAccessSelector, boolean, string>;
private allScopes: DataLoader<
OrganizationUserScopesSelector,
ReadonlyArray<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>,
@ -74,7 +73,7 @@ export class OrganizationAccess {
this.logger = logger.child({
source: 'OrganizationAccess',
});
this.userAccess = new Dataloader(
this.userAccess = new DataLoader(
async selectors => {
const scopes = await this.scopes.loadMany(selectors);
@ -104,7 +103,7 @@ export class OrganizationAccess {
},
},
);
this.tokenAccess = new Dataloader(
this.tokenAccess = new DataLoader(
selectors =>
Promise.all(
selectors.map(async selector => {
@ -128,7 +127,7 @@ export class OrganizationAccess {
},
},
);
this.allScopes = new Dataloader(
this.allScopes = new DataLoader(
async selectors => {
const scopesPerSelector = await this.storage.getOrganizationMemberAccessPairs(selectors);
@ -144,7 +143,7 @@ export class OrganizationAccess {
},
},
);
this.scopes = new Dataloader(
this.scopes = new DataLoader(
async selectors => {
const scopesPerSelector = await this.allScopes.loadMany(selectors);
@ -174,7 +173,7 @@ export class OrganizationAccess {
},
);
this.ownership = new Dataloader(
this.ownership = new DataLoader(
async selectors => {
const ownerPerSelector = await Promise.all(
selectors.map(selector => this.storage.getOrganizationOwnerId(selector)),
@ -192,7 +191,7 @@ export class OrganizationAccess {
},
);
this.tokenInfo = new Dataloader(
this.tokenInfo = new DataLoader(
selectors => Promise.all(selectors.map(selector => this.tokenStorage.getToken(selector))),
{
cacheKeyFn(selector) {

View file

@ -78,24 +78,21 @@ export const resolvers: AuthModule.Resolvers & {
TOKENS_WRITE: TargetAccessScope.TOKENS_WRITE,
},
Member: {
temporaryFixId(member) {
return `${member.organization}:${member.id}`;
},
organizationAccessScopes(member, _, { injector }) {
return injector.get(AuthManager).getMemberOrganizationScopes({
user: member.id,
user: member.user.id,
organization: member.organization,
});
},
projectAccessScopes(member, _, { injector }) {
return injector.get(AuthManager).getMemberProjectScopes({
user: member.id,
user: member.user.id,
organization: member.organization,
});
},
targetAccessScopes(member, _, { injector }) {
return injector.get(AuthManager).getMemberTargetScopes({
user: member.id,
user: member.user.id,
organization: member.organization,
});
},

View file

@ -404,7 +404,7 @@ export default gql`
input AssignMemberRoleInput {
organization: ID!
member: ID!
user: ID!
role: ID!
}
@ -444,7 +444,7 @@ export default gql`
input AssignMemberRoleMigrationInput {
organization: ID!
role: ID!
members: [ID!]!
users: [ID!]!
}
input CreateMemberRoleMigrationInput {
@ -454,7 +454,7 @@ export default gql`
organizationScopes: [OrganizationAccessScope!]!
projectScopes: [ProjectAccessScope!]!
targetScopes: [TargetAccessScope!]!
members: [ID!]!
users: [ID!]!
}
type MigrateUnassignedMembersResult {

View file

@ -642,7 +642,7 @@ export class OrganizationManager {
const { code } = await this.storage.createOrganizationTransferRequest({
organization: organization.id,
user: member.id,
user: member.user.id,
});
await this.emails.schedule({
@ -985,7 +985,7 @@ export class OrganizationManager {
};
}
async assignMemberRole(input: { organizationId: string; memberId: string; roleId: string }) {
async assignMemberRole(input: { organizationId: string; userId: string; roleId: string }) {
await this.authManager.ensureOrganizationAccess({
organization: input.organizationId,
scope: OrganizationAccessScope.MEMBERS,
@ -994,7 +994,7 @@ export class OrganizationManager {
// Ensure selected member is part of the organization
const member = await this.storage.getOrganizationMember({
organization: input.organizationId,
user: input.memberId,
user: input.userId,
});
if (!member) {
@ -1073,7 +1073,7 @@ export class OrganizationManager {
// Assign the role to the member
await this.storage.assignOrganizationMemberRole({
organizationId: input.organizationId,
userId: input.memberId,
userId: input.userId,
roleId: input.roleId,
});
@ -1084,7 +1084,7 @@ export class OrganizationManager {
ok: {
updatedMember: await this.getOrganizationMember({
organization: input.organizationId,
user: input.memberId,
user: input.userId,
}),
previousMemberRole: member.role,
},
@ -1409,7 +1409,7 @@ export class OrganizationManager {
organizationId: string;
assignRole?: {
role: string;
members: readonly string[];
users: readonly string[];
} | null;
createRole?: {
name: string;
@ -1417,7 +1417,7 @@ export class OrganizationManager {
organizationScopes: readonly OrganizationAccessScope[];
projectScopes: readonly ProjectAccessScope[];
targetScopes: readonly TargetAccessScope[];
members: readonly string[];
users: readonly string[];
} | null;
}) {
const currentUser = await this.authManager.getCurrentUser();
@ -1438,7 +1438,7 @@ export class OrganizationManager {
return this.assignRoleToMembersMigration({
organizationId,
roleId: assignRole.role,
members: assignRole.members,
users: assignRole.users,
});
}
@ -1459,7 +1459,7 @@ export class OrganizationManager {
organizationScopes: readonly OrganizationAccessScope[];
projectScopes: readonly ProjectAccessScope[];
targetScopes: readonly TargetAccessScope[];
members: readonly string[];
users: readonly string[];
}) {
const result = await this.createMemberRole({
organizationId: input.organizationId,
@ -1474,7 +1474,7 @@ export class OrganizationManager {
return this.assignRoleToMembersMigration({
roleId: result.ok.createdRole.id,
organizationId: input.organizationId,
members: input.members,
users: input.users,
});
}
@ -1484,12 +1484,12 @@ export class OrganizationManager {
private async assignRoleToMembersMigration(input: {
organizationId: string;
roleId: string;
members: readonly string[];
users: readonly string[];
}) {
await this.storage.assignOrganizationMemberRoleToMany({
organizationId: input.organizationId,
roleId: input.roleId,
userIds: input.members,
userIds: input.users,
});
return {

View file

@ -448,7 +448,7 @@ export const resolvers: OrganizationModule.Resolvers = {
return injector.get(OrganizationManager).assignMemberRole({
organizationId,
memberId: input.member,
userId: input.user,
roleId: input.role,
});
},
@ -591,7 +591,7 @@ export const resolvers: OrganizationModule.Resolvers = {
async canLeaveOrganization(member, _, { injector }) {
const { result } = await injector.get(OrganizationManager).canLeaveOrganization({
organizationId: member.organization,
userId: member.id,
userId: member.user.id,
});
return result;

View file

@ -810,7 +810,7 @@ function createSchemaChangeId(change: { type: string; meta: Record<string, unkno
}
const ApprovalMetadataModel = z.object({
userId: z.string(),
userId: z.string().uuid(),
schemaCheckId: z.string(),
date: z.string(),
});

View file

@ -169,7 +169,6 @@ const AdminStatsQuery = graphql(`
name
owner {
id
temporaryFixId
user {
id
email
@ -178,7 +177,6 @@ const AdminStatsQuery = graphql(`
members {
nodes {
id
temporaryFixId
user {
id
email

View file

@ -33,7 +33,6 @@ const ProjectLayoutQuery = graphql(`
name
me {
id
temporaryFixId
...CanAccessProject_MemberFragment
}
projects {

View file

@ -37,7 +37,6 @@ const TargetLayoutQuery = graphql(`
name
me {
id
temporaryFixId
...CanAccessTarget_MemberFragment
}
projects {

View file

@ -264,7 +264,9 @@ const UsePermissionManager_OrganizationFragment = graphql(`
const UsePermissionManager_MemberFragment = graphql(`
fragment UsePermissionManager_MemberFragment on Member {
id
temporaryFixId
user {
id
}
targetAccessScopes
projectAccessScopes
organizationAccessScopes
@ -312,7 +314,7 @@ export function usePermissionsManager({
const result = await mutate({
input: {
organization: organization.cleanId,
user: member.id,
user: member.user.id,
targetScopes,
projectScopes,
organizationScopes,

View file

@ -36,7 +36,6 @@ const OrganizationMemberRoleSwitcher_AssignRoleMutation = graphql(`
ok {
updatedMember {
id
temporaryFixId
user {
id
displayName
@ -66,7 +65,6 @@ const OrganizationMemberRoleSwitcher_OrganizationFragment = graphql(`
cleanId
me {
id
temporaryFixId
isAdmin
organizationAccessScopes
projectAccessScopes
@ -74,7 +72,6 @@ const OrganizationMemberRoleSwitcher_OrganizationFragment = graphql(`
}
owner {
id
temporaryFixId
}
memberRoles {
id
@ -98,10 +95,12 @@ const OrganizationMemberRoleSwitcher_OrganizationFragment = graphql(`
const OrganizationMemberRoleSwitcher_MemberFragment = graphql(`
fragment OrganizationMemberRoleSwitcher_MemberFragment on Member {
id
temporaryFixId
organizationAccessScopes
projectAccessScopes
targetAccessScopes
user {
id
}
...ChangePermissionsModal_MemberFragment
}
`);
@ -131,7 +130,7 @@ function OrganizationMemberRoleSwitcher(props: {
const [isPermissionsModalOpen, togglePermissionsModalOpen] = useToggle(false);
const memberRole = roles.find(role => role.id === props.memberRoleId);
if (!memberRole && !member) {
if (!memberRole || !member) {
console.error('No role or member provided to OrganizationMemberRoleSwitcher');
return null;
}
@ -150,7 +149,7 @@ function OrganizationMemberRoleSwitcher(props: {
input: {
organization: organization.cleanId,
role: role.id,
member: props.memberId,
user: member.user.id,
},
});
@ -321,7 +320,6 @@ const OrganizationMemberRow_DeleteMember = graphql(`
const OrganizationMemberRow_MemberFragment = graphql(`
fragment OrganizationMemberRow_MemberFragment on Member {
id
temporaryFixId
user {
id
provider

View file

@ -324,7 +324,7 @@ function SimilarRoles(props: {
const migrationFormSchema = z.intersection(
z.object({
members: z.array(z.string()).min(1),
users: z.array(z.string()).min(1),
}),
z.union([
roleFormSchema,
@ -363,7 +363,9 @@ const OrganizationMemberRolesMigrationGroup_Migrate = graphql(`
members {
nodes {
id
temporaryFixId
user {
id
}
organizationAccessScopes
projectAccessScopes
targetAccessScopes
@ -413,7 +415,7 @@ function OrganizationMemberRolesMigrationGroup(props: {
mode: 'onChange',
resolver: zodResolver(migrationFormSchema),
defaultValues: {
members: memberGroup.members.map(m => m.id),
users: memberGroup.members.map(m => m.user.id),
roleId: '',
name: '',
description: '',
@ -458,7 +460,7 @@ function OrganizationMemberRolesMigrationGroup(props: {
assignRole: {
organization: props.organizationCleanId,
role: data.roleId,
members: data.members,
users: data.users,
},
}
: {
@ -476,7 +478,7 @@ function OrganizationMemberRolesMigrationGroup(props: {
targetScopes: data.targetScopes.filter((s): s is TargetAccessScope =>
Object.values(TargetAccessScope).includes(s as TargetAccessScope),
),
members: data.members,
users: data.users,
},
},
});

View file

@ -13,7 +13,7 @@ const ChangePermissionsModal_OrganizationFragment = graphql(`
export const ChangePermissionsModal_MemberFragment = graphql(`
fragment ChangePermissionsModal_MemberFragment on Member {
temporaryFixId
id
...UsePermissionManager_MemberFragment
}
`);

View file

@ -34,7 +34,7 @@ const TransferOrganizationOwnership_Members = graphql(`
name
members {
nodes {
temporaryFixId
id
isOwner
...MemberFields
user {
@ -54,7 +54,6 @@ const TransferOrganizationOwnership_Members = graphql(`
const MemberFields = graphql(`
fragment MemberFields on Member {
id
temporaryFixId
user {
id
fullName
@ -159,7 +158,7 @@ export const TransferOrganizationOwnershipModal = ({
const onSelect = useCallback(
(member: Member) => {
setSelected(member);
void setFieldValue('newOwner', member.id, true);
void setFieldValue('newOwner', member.user.id, true);
},
[setSelected, setFieldValue],
);

View file

@ -7,7 +7,6 @@ export { OrganizationAccessScope };
const CanAccessOrganization_MemberFragment = graphql(`
fragment CanAccessOrganization_MemberFragment on Member {
id
temporaryFixId
organizationAccessScopes
}
`);

View file

@ -7,7 +7,6 @@ export { TargetAccessScope };
export const CanAccessTarget_MemberFragment = graphql(`
fragment CanAccessTarget_MemberFragment on Member {
id
temporaryFixId
targetAccessScopes
}
`);

View file

@ -35,10 +35,6 @@ export const urqlClient = createClient({
Mutation,
},
keys: {
// Member.id is not globally unique, it's really User.id
// In order to avoid conflicts or cache issues, let's use Member.temporaryFixId.
// This is a temporary solution until we have a better way to handle this.
Member: ({ temporaryFixId }) => `Member:${temporaryFixId}`,
RequestsOverTime: noKey,
FailuresOverTime: noKey,
DurationOverTime: noKey,

View file

@ -19,7 +19,7 @@ const OrganizationPolicyPageQuery = graphql(`
organization {
id
me {
temporaryFixId
id
...CanAccessOrganization_MemberFragment
}
projects {

View file

@ -23,7 +23,6 @@ const OrganizationTransferPage_GetRequest = graphql(`
name
owner {
id
temporaryFixId
user {
id
displayName

View file

@ -18,7 +18,6 @@ const ProjectPolicyPageQuery = graphql(`
id
me {
id
temporaryFixId
...CanAccessProject_MemberFragment
}
}

View file

@ -827,7 +827,6 @@ const TargetLaboratoryPageQuery = graphql(`
id
me {
id
temporaryFixId
...CanAccessTarget_MemberFragment
}
}

View file

@ -1033,7 +1033,6 @@ const TargetSettingsPageQuery = graphql(`
cleanId
...TargetSettingsPage_OrganizationFragment
me {
temporaryFixId
...CDNAccessTokens_MeFragment
}
}

View file

@ -1,211 +0,0 @@
/// @ts-check
const {
requireGraphQLSchemaFromContext,
requireSiblingsOperations,
} = require('@graphql-eslint/eslint-plugin');
const {
Kind,
TypeInfo,
visit,
visitWithTypeInfo,
GraphQLObjectType,
GraphQLInterfaceType,
GraphQLUnionType,
isListType,
isNonNullType,
} = require('graphql');
function getBaseType(type) {
if (isNonNullType(type) || isListType(type)) {
return getBaseType(type.ofType);
}
return type;
}
const RULE_ID = 'graphql-require-selections';
const idNames = ['temporaryFixId'];
/// Ported https://github.com/dimaMachina/graphql-eslint/blob/3c1020888472eb6579ffddc1e8e5ec16df8fad74/packages/plugin/src/rules/require-selections.ts
/**
* @type {import('@graphql-eslint/eslint-plugin').GraphQLESLintRule}
*/
const rule = {
meta: {
type: 'problem',
hasSuggestions: true,
messages: {
[RULE_ID]:
"Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}.",
},
docs: {
category: 'Operations',
description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
requiresSchema: true,
requiresSiblings: true,
},
schema: [],
},
create(context) {
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
const siblings = requireSiblingsOperations(RULE_ID, context);
// Check selections only in OperationDefinition,
// skip selections of OperationDefinition and InlineFragment
const selector =
'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]';
const typeInfo = new TypeInfo(schema);
function checkFragments(node) {
for (const selection of node.selections) {
if (selection.kind !== Kind.FRAGMENT_SPREAD) {
continue;
}
const [foundSpread] = siblings.getFragment(selection.name.value);
if (!foundSpread) {
continue;
}
const checkedFragmentSpreads = new Set();
const visitor = visitWithTypeInfo(typeInfo, {
SelectionSet(node, key, _parent) {
const parent = _parent;
if (parent.kind === Kind.FRAGMENT_DEFINITION) {
checkedFragmentSpreads.add(parent.name.value);
} else if (parent.kind !== Kind.INLINE_FRAGMENT) {
checkSelections(
node,
typeInfo.getType(),
selection.loc.start,
parent,
checkedFragmentSpreads,
);
}
},
});
visit(foundSpread.document, visitor);
}
}
function checkSelections(
node,
type,
// Fragment can be placed in separate file
// Provide actual fragment spread location instead of location in fragment
loc,
// Can't access to node.parent in GraphQL AST.Node, so pass as argument
parent,
checkedFragmentSpreads = new Set(),
) {
const rawType = getBaseType(type);
if (rawType instanceof GraphQLObjectType || rawType instanceof GraphQLInterfaceType) {
checkFields(rawType);
} else if (rawType instanceof GraphQLUnionType) {
for (const selection of node.selections) {
if (selection.kind === Kind.INLINE_FRAGMENT) {
const types = rawType.getTypes();
const t = types.find(t => t.name === selection.typeCondition.name.value);
if (t) {
checkFields(t);
}
}
}
}
function checkFields(rawType) {
const fields = rawType.getFields();
const hasIdFieldInType = idNames.some(name => fields[name]);
if (!hasIdFieldInType) {
return;
}
function hasIdField({ selections }) {
return selections.some(selection => {
if (selection.kind === Kind.FIELD) {
if (selection.alias && idNames.includes(selection.alias.value)) {
return true;
}
return idNames.includes(selection.name.value);
}
if (selection.kind === Kind.INLINE_FRAGMENT) {
return hasIdField(selection.selectionSet);
}
if (selection.kind === Kind.FRAGMENT_SPREAD) {
const [foundSpread] = siblings.getFragment(selection.name.value);
if (foundSpread) {
const fragmentSpread = foundSpread.document;
checkedFragmentSpreads.add(fragmentSpread.name.value);
return hasIdField(fragmentSpread.selectionSet);
}
}
return false;
});
}
const hasId = hasIdField(node);
checkFragments(node);
if (hasId) {
return;
}
const pluralSuffix = idNames.length > 1 ? 's' : '';
const fieldName = idNames.join(',');
const addition =
checkedFragmentSpreads.size === 0
? ''
: ` or add to used fragment${
checkedFragmentSpreads.size > 1 ? 's' : ''
} ${Array.from(checkedFragmentSpreads).join(', ')}`;
const problem = {
loc,
messageId: RULE_ID,
data: {
pluralSuffix,
fieldName,
addition,
},
};
// Don't provide suggestions for selections in fragments as fragment can be in a separate file
if ('type' in node) {
problem.suggest = idNames.map(idName => ({
desc: `Add \`${idName}\` selection`,
fix: fixer => {
let insertNode = node.selections[0];
insertNode =
insertNode.kind === Kind.INLINE_FRAGMENT
? insertNode.selectionSet.selections[0]
: insertNode;
return fixer.insertTextBefore(insertNode, `${idName} `);
},
}));
}
context.report(problem);
}
}
return {
[selector](node) {
const typeInfo = node.typeInfo();
if (typeInfo.gqlType) {
checkSelections(node, typeInfo.gqlType, node.loc.start, node.parent);
}
},
};
},
};
module.exports = rule;

View file

@ -1,6 +1,5 @@
module.exports = {
rules: {
'enforce-deps-in-dev': require('./enforce-deps-in-dev.cjs'),
'graphql-require-selection': require('./graphql-require-selection.cjs'),
},
};

View file

@ -6,7 +6,7 @@
"types": ["vitest/globals"],
"baseUrl": ".",
"outDir": "dist",
"rootDir": "packages",
"rootDir": ".",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
@ -66,7 +66,8 @@
"./packages/libraries/external-composition/src/index.ts"
],
"@graphql-hive/core": ["./packages/libraries/core/src/index.ts"],
"@/*": ["./packages/web/app/src/*"]
"@/*": ["./packages/web/app/src/*"],
"testkit/*": ["./integration-tests/testkit/*"]
}
},
"include": ["packages", "tsup.config.node.ts"],