Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b0544e8
feat: add data source for api/src/guidance-tag
lcampbell2 Apr 14, 2026
a666a8a
fix tests
lcampbell2 Apr 14, 2026
46f8f0a
Merge branch 'master' into feat/add-organization-data-source
lcampbell2 Apr 15, 2026
f3ce474
create data source and add to context
lcampbell2 Apr 15, 2026
192c261
update resolvers to use data source
lcampbell2 Apr 15, 2026
6e1e6c6
update mutations to use data source
lcampbell2 Apr 15, 2026
65a3409
fix tests
lcampbell2 Apr 16, 2026
c9db127
add translations
lcampbell2 Apr 16, 2026
852509c
fix tests
lcampbell2 Apr 16, 2026
1b69253
create DomainDataSource
lcampbell2 Apr 16, 2026
f995bc6
update resolvers and queries
lcampbell2 Apr 16, 2026
ae96b0c
update mutations
lcampbell2 Apr 16, 2026
50b7cf3
replace loaders with data sources
lcampbell2 Apr 17, 2026
6127948
update resolver tests
lcampbell2 Apr 20, 2026
e0903a4
Merge branch 'master' into feat/add-domain-data-source
lcampbell2 May 11, 2026
47d7701
fix merge errors
lcampbell2 May 20, 2026
ac5bf61
Merge branch 'master' into feat/add-domain-data-source
lcampbell2 Jun 16, 2026
ed7ca38
add favourite and ownership checks to domian DS
lcampbell2 Jun 17, 2026
d7489b8
refactor(domain): move claim/filter checks into DomainDataSource and …
lcampbell2 Jun 17, 2026
7e0768d
refactor(domain): route mutation audit logging through dataSources an…
lcampbell2 Jun 17, 2026
029af21
test(domain): finish DS-first mutation context migration and prune ob…
lcampbell2 Jun 17, 2026
1b9fd00
test(domain): retain required loader shims in bulk update specs and r…
lcampbell2 Jun 17, 2026
e4409c7
fix(domain): use byTagId.loadMany in claimTags resolver and keep DS q…
lcampbell2 Jun 17, 2026
a802034
fix linting errors
lcampbell2 Jun 17, 2026
f6518d0
test(api): align DS resolver test contexts and fix request-scan lint …
lcampbell2 Jun 17, 2026
0663826
feat(affiliation): add AffiliationDataSource wrapper for affiliation …
lcampbell2 Jun 17, 2026
6c4b327
feat(context): add affiliation data source to createContext
lcampbell2 Jun 17, 2026
78e2327
refactor(resolvers): use affiliation data source for object affiliati…
lcampbell2 Jun 18, 2026
8ccb31f
refactor(affiliation): centralize affiliation transactions in datasou…
lcampbell2 Jun 18, 2026
cf07b23
refactor(affiliation): remove legacy loader wiring from context tests
lcampbell2 Jun 18, 2026
f349809
refactor(affiliation): route mutation audit logging through datasource
lcampbell2 Jun 18, 2026
0ba4682
update mutation tests
lcampbell2 Jun 18, 2026
57f4b7d
Merge branch 'master' into feat/add-affiliation-data-source
lcampbell2 Jun 18, 2026
ebfa3e4
Merge branch 'feat/add-affiliation-data-source' into feat/add-user-da…
lcampbell2 Jun 22, 2026
0d1ab05
feat(user): add UserDataSource and export it
lcampbell2 Jun 22, 2026
e16398a
feat(context): wire user datasource into request context
lcampbell2 Jun 22, 2026
629aa25
refactor(user-queries): use context.dataSources.user
lcampbell2 Jun 22, 2026
3f134a3
refactor(user-auth): move signin/auth flows to datasource
lcampbell2 Jun 22, 2026
93c9751
refactor(user-profile): migrate profile and MFA mutations
lcampbell2 Jun 22, 2026
820861c
refactor(user-account): migrate signup and account closure
lcampbell2 Jun 22, 2026
6909a79
test(user): centralize datasource test context wiring
lcampbell2 Jun 22, 2026
1ed6a02
test(user): centralize datasource test context wiring
lcampbell2 Jun 22, 2026
8880845
refactor(user): simplify auth mutation control flow
lcampbell2 Jun 23, 2026
9b1967d
test(user): drop french translation-only assertions and keep transact…
lcampbell2 Jun 23, 2026
f9f5ff7
FAIL src/user/mutations/__tests__/remove-phone-number.test.js
lcampbell2 Jun 23, 2026
801cbc8
fix linting errors
lcampbell2 Jun 23, 2026
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
20 changes: 20 additions & 0 deletions api/src/__tests__/create-context.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,25 @@ describe('given the create context function', () => {

expect(context.userKey).toEqual('1234')
})

it('returns object with affiliation data source', async () => {
const context = await createContext({
query: jest.fn(),
transaction: jest.fn(),
collections: [],
req: {
language: 'en',
headers: {
authorization: tokenize({parameters: {userKey: '1234'}}),
},
},
res: {},
})

expect(context.dataSources.affiliation).toBeDefined()
expect(context.dataSources.affiliation.byKey).toBeDefined()
expect(context.dataSources.affiliation.connectionsByUserId).toBeDefined()
expect(context.dataSources.affiliation.connectionsByOrgId).toBeDefined()
})
})
})
3 changes: 0 additions & 3 deletions api/src/__tests__/initialize-loaders.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ describe('initializeLoaders', () => {
'loadUserConnectionsByUserId',
'loadUserByKey',
'loadMyTrackerByUserId',
'loadAffiliationByKey',
'loadAffiliationConnectionsByUserId',
'loadAffiliationConnectionsByOrgId',
'loadVerifiedDomainsById',
'loadVerifiedDomainByKey',
'loadVerifiedDomainConnections',
Expand Down
227 changes: 227 additions & 0 deletions api/src/affiliation/data-source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import {
loadAffiliationByKey,
loadAffiliationConnectionsByOrgId,
loadAffiliationConnectionsByUserId,
} from './loaders'

export class AffiliationDataSource {
constructor({ query, userKey, i18n, language, cleanseInput, transaction, collections }) {
this._query = query
this._transaction = transaction
this._collections = collections

this.byKey = loadAffiliationByKey({ query, userKey, i18n })
this.connectionsByUserId = loadAffiliationConnectionsByUserId({
query,
language,
userKey,
cleanseInput,
i18n,
})
this.connectionsByOrgId = loadAffiliationConnectionsByOrgId({
query,
userKey,
cleanseInput,
i18n,
})
}

async affiliationByOrgAndUser({ orgId, userId }) {
let affiliationCursor
try {
affiliationCursor = await this._query`
WITH affiliations, organizations, users
FOR v, e IN 1..1 OUTBOUND ${orgId} affiliations
FILTER e._to == ${userId}
RETURN e
`
} catch (err) {
err.affiliationDataSourceOp = 'query'
throw err
}

if (affiliationCursor.count < 1) {
return undefined
}

try {
return await affiliationCursor.next()
} catch (err) {
err.affiliationDataSourceOp = 'cursor'
throw err
}
}

async createAffiliation({ orgId, userId, permission }) {
const transaction = await this._transaction(this._collections)

try {
await transaction.step(
() =>
this._query`
WITH affiliations, organizations, users
INSERT {
_from: ${orgId},
_to: ${userId},
permission: ${permission},
} INTO affiliations
`,
)
} catch (err) {
if (typeof transaction.abort === 'function') await transaction.abort()
err.affiliationDataSourceOp = 'trx-step'
throw err
}

try {
await transaction.commit()
} catch (err) {
if (typeof transaction.abort === 'function') await transaction.abort()
err.affiliationDataSourceOp = 'trx-commit'
throw err
}
}

async createPendingAffiliation({ orgId, userId }) {
return this.createAffiliation({ orgId, userId, permission: 'pending' })
}

async updateAffiliationPermission({ affiliationKey, orgId, userId, permission }) {
const trx = await this._transaction(this._collections)

const edge = {
_from: orgId,
_to: userId,
permission,
}

try {
await trx.step(
async () =>
await this._query`
WITH affiliations, organizations, users
UPSERT { _key: ${affiliationKey} }
INSERT ${edge}
UPDATE ${edge}
IN affiliations
`,
)
} catch (err) {
if (typeof trx.abort === 'function') await trx.abort()
err.affiliationDataSourceOp = 'trx-step'
throw err
}

try {
await trx.commit()
} catch (err) {
if (typeof trx.abort === 'function') await trx.abort()
err.affiliationDataSourceOp = 'trx-commit'
throw err
}
}

async removeAffiliation({ orgId, userId }) {
const trx = await this._transaction(this._collections)

try {
await trx.step(
() =>
this._query`
WITH affiliations, organizations, users
FOR aff IN affiliations
FILTER aff._from == ${orgId}
FILTER aff._to == ${userId}
REMOVE aff IN affiliations
RETURN true
`,
)
} catch (err) {
if (typeof trx.abort === 'function') await trx.abort()
err.affiliationDataSourceOp = 'trx-step'
throw err
}

try {
await trx.commit()
} catch (err) {
if (typeof trx.abort === 'function') await trx.abort()
err.affiliationDataSourceOp = 'trx-commit'
throw err
}
}

async transferOrgOwnership({ orgId, fromUserId, toUserId }) {
const trx = await this._transaction(this._collections)

try {
await trx.step(
() =>
this._query`
WITH affiliations, organizations, users
FOR aff IN affiliations
FILTER aff._from == ${orgId}
FILTER aff._to == ${fromUserId}
UPDATE { _key: aff._key } WITH {
permission: "admin",
} IN affiliations
RETURN aff
`,
)
} catch (err) {
if (typeof trx.abort === 'function') await trx.abort()
err.affiliationDataSourceOp = 'trx-step'
throw err
}

try {
await trx.step(
() =>
this._query`
WITH affiliations, organizations, users
FOR aff IN affiliations
FILTER aff._from == ${orgId}
FILTER aff._to == ${toUserId}
UPDATE { _key: aff._key } WITH {
permission: "owner",
} IN affiliations
RETURN aff
`,
)
} catch (err) {
if (typeof trx.abort === 'function') await trx.abort()
err.affiliationDataSourceOp = 'trx-step'
throw err
}

try {
await trx.commit()
} catch (err) {
if (typeof trx.abort === 'function') await trx.abort()
err.affiliationDataSourceOp = 'trx-commit'
throw err
}
}

async orgAdminUserKeys({ orgId }) {
let orgAdminsCursor
try {
orgAdminsCursor = await this._query`
WITH affiliations, organizations, users
FOR v, e IN 1..1 OUTBOUND ${orgId} affiliations
FILTER e.permission IN ["admin", "owner", "super_admin"]
RETURN v._key
`
} catch (err) {
err.affiliationDataSourceOp = 'query'
throw err
}

try {
return await orgAdminsCursor.all()
} catch (err) {
err.affiliationDataSourceOp = 'cursor'
throw err
}
}
}
1 change: 1 addition & 0 deletions api/src/affiliation/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './data-source'
export * from './loaders'
export * from './mutations'
export * from './objects'
37 changes: 32 additions & 5 deletions api/src/affiliation/mutations/__tests__/invite-user-to-org.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { dbNameFromFile } from 'arango-tools'
import { ensureDatabase as ensure } from '../../../testUtilities'
import { setupI18n } from '@lingui/core'
import { graphql, GraphQLSchema } from 'graphql'
import { graphql as executeGraphql, GraphQLSchema } from 'graphql'
import { toGlobalId } from 'graphql-relay'

import englishMessages from '../../../locale/en/messages'
Expand All @@ -12,11 +12,38 @@ import { createQuerySchema } from '../../../query'
import { cleanseInput } from '../../../validators'
import { loadOrgByKey, loadOrganizationNamesById } from '../../../organization/loaders'
import { loadUserByKey, loadUserByUserName } from '../../../user/loaders'
import { AffiliationDataSource } from '../../data-source'
import dbschema from '../../../../database.json'
import { collectionNames } from '../../../collection-names'

const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env

const withAffiliationDataSource = (contextValue = {}) => {
const dataSources = contextValue.dataSources || {}
if (dataSources.affiliation && dataSources.auditLogs) return contextValue

return {
...contextValue,
dataSources: {
...dataSources,
affiliation:
dataSources.affiliation ||
new AffiliationDataSource({
query: contextValue.query,
transaction: contextValue.transaction,
collections: contextValue.collections,
userKey: contextValue.userKey,
i18n: contextValue.i18n,
language: contextValue.request?.language,
cleanseInput: contextValue.validators?.cleanseInput,
}),
auditLogs: dataSources.auditLogs || { logActivity: jest.fn().mockResolvedValue(undefined) },
},
}
}

const graphql = (args) => executeGraphql({ ...args, contextValue: withAffiliationDataSource(args.contextValue) })

describe('invite user to org', () => {
let query, drop, truncate, schema, collections, transaction, i18n, tokenize, user, org, userToInvite

Expand Down Expand Up @@ -1622,7 +1649,7 @@ describe('invite user to org', () => {
query,
collections: collectionNames,
transaction: jest.fn().mockReturnValue({
step: jest.fn().mockRejectedValue('trx step err'),
step: jest.fn().mockRejectedValue(new Error('trx step err')),
abort: jest.fn(),
}),
userKey: 123,
Expand Down Expand Up @@ -1659,7 +1686,7 @@ describe('invite user to org', () => {

expect(response).toEqual(error)
expect(consoleOutput).toEqual([
`Transaction step error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: trx step err`,
`Transaction step error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: Error: trx step err`,
])
})
})
Expand Down Expand Up @@ -1700,7 +1727,7 @@ describe('invite user to org', () => {
collections: collectionNames,
transaction: jest.fn().mockReturnValue({
step: jest.fn(),
commit: jest.fn().mockRejectedValue('trx commit err'),
commit: jest.fn().mockRejectedValue(new Error('trx commit err')),
abort: jest.fn(),
}),
userKey: 123,
Expand Down Expand Up @@ -1740,7 +1767,7 @@ describe('invite user to org', () => {

expect(response).toEqual(error)
expect(consoleOutput).toEqual([
`Transaction commit error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: trx commit err`,
`Transaction commit error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: Error: trx commit err`,
])
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setupI18n } from '@lingui/core'
import { dbNameFromFile } from 'arango-tools'
import { ensureDatabase as ensure } from '../../../testUtilities'
import { graphql, GraphQLSchema, GraphQLError } from 'graphql'
import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql'
import { toGlobalId } from 'graphql-relay'

import englishMessages from '../../../locale/en/messages'
Expand All @@ -12,11 +12,34 @@ import { loadUserByKey } from '../../../user/loaders'
import { cleanseInput } from '../../../validators'
import { createMutationSchema } from '../../../mutation'
import { createQuerySchema } from '../../../query'
import { AffiliationDataSource } from '../../data-source'
import dbschema from '../../../../database.json'
import { collectionNames } from '../../../collection-names'

const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env

const withAffiliationDataSource = (contextValue = {}) => {
if (contextValue.dataSources?.affiliation) return contextValue

return {
...contextValue,
dataSources: {
...(contextValue.dataSources || {}),
affiliation: new AffiliationDataSource({
query: contextValue.query,
transaction: contextValue.transaction,
collections: contextValue.collections,
userKey: contextValue.userKey,
i18n: contextValue.i18n,
language: contextValue.request?.language,
cleanseInput: contextValue.validators?.cleanseInput,
}),
},
}
}

const graphql = (args) => executeGraphql({ ...args, contextValue: withAffiliationDataSource(args.contextValue) })

describe('given a successful leave', () => {
let query, drop, truncate, schema, collections, transaction, i18n, user, org, domain, domain2

Expand Down
Loading