mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #2459 from gradido/2428-feature-federation-implement-multiple-apollo-graphql-endpoints
feat(backend): federation implement multiple apollo graphql endpoints
This commit is contained in:
commit
c5bb2ae380
@ -7,6 +7,7 @@ class EventProtocolEmitter {
|
|||||||
/* }extends EventEmitter { */
|
/* }extends EventEmitter { */
|
||||||
private events: Event[]
|
private events: Event[]
|
||||||
|
|
||||||
|
/*
|
||||||
public addEvent(event: Event) {
|
public addEvent(event: Event) {
|
||||||
this.events.push(event)
|
this.events.push(event)
|
||||||
}
|
}
|
||||||
@ -14,6 +15,7 @@ class EventProtocolEmitter {
|
|||||||
public getEvents(): Event[] {
|
public getEvents(): Event[] {
|
||||||
return this.events
|
return this.events
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
public isDisabled() {
|
public isDisabled() {
|
||||||
logger.info(`EventProtocol - isDisabled=${CONFIG.EVENT_PROTOCOL_DISABLED}`)
|
logger.info(`EventProtocol - isDisabled=${CONFIG.EVENT_PROTOCOL_DISABLED}`)
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
declare module '@hyperswarm/dht'
|
|
||||||
@ -1,798 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
|
|
||||||
import { startDHT } from './index'
|
|
||||||
import DHT from '@hyperswarm/dht'
|
|
||||||
import CONFIG from '@/config'
|
|
||||||
import { logger } from '@test/testSetup'
|
|
||||||
import { Community as DbCommunity } from '@entity/Community'
|
|
||||||
import { testEnvironment, cleanDB } from '@test/helpers'
|
|
||||||
|
|
||||||
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
|
|
||||||
|
|
||||||
jest.mock('@hyperswarm/dht')
|
|
||||||
|
|
||||||
const TEST_TOPIC = 'gradido_test_topic'
|
|
||||||
|
|
||||||
const keyPairMock = {
|
|
||||||
publicKey: Buffer.from('publicKey'),
|
|
||||||
secretKey: Buffer.from('secretKey'),
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverListenSpy = jest.fn()
|
|
||||||
|
|
||||||
const serverEventMocks: { [key: string]: any } = {}
|
|
||||||
|
|
||||||
const serverOnMock = jest.fn().mockImplementation((key: string, callback) => {
|
|
||||||
serverEventMocks[key] = callback
|
|
||||||
})
|
|
||||||
|
|
||||||
const nodeCreateServerMock = jest.fn().mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
on: serverOnMock,
|
|
||||||
listen: serverListenSpy,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const nodeAnnounceMock = jest.fn().mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
finished: jest.fn(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const lookupResultMock = {
|
|
||||||
token: Buffer.from(TEST_TOPIC),
|
|
||||||
from: {
|
|
||||||
id: Buffer.from('somone'),
|
|
||||||
host: '188.95.53.5',
|
|
||||||
port: 63561,
|
|
||||||
},
|
|
||||||
to: { id: null, host: '83.53.31.27', port: 55723 },
|
|
||||||
peers: [
|
|
||||||
{
|
|
||||||
publicKey: Buffer.from('some-public-key'),
|
|
||||||
relayAddresses: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeLookupMock = jest.fn().mockResolvedValue([lookupResultMock])
|
|
||||||
|
|
||||||
const socketEventMocks: { [key: string]: any } = {}
|
|
||||||
|
|
||||||
const socketOnMock = jest.fn().mockImplementation((key: string, callback) => {
|
|
||||||
socketEventMocks[key] = callback
|
|
||||||
})
|
|
||||||
|
|
||||||
const socketWriteMock = jest.fn()
|
|
||||||
|
|
||||||
const nodeConnectMock = jest.fn().mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
on: socketOnMock,
|
|
||||||
once: socketOnMock,
|
|
||||||
write: socketWriteMock,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
DHT.hash.mockImplementation(() => {
|
|
||||||
return Buffer.from(TEST_TOPIC)
|
|
||||||
})
|
|
||||||
|
|
||||||
DHT.keyPair.mockImplementation(() => {
|
|
||||||
return keyPairMock
|
|
||||||
})
|
|
||||||
|
|
||||||
DHT.mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
createServer: nodeCreateServerMock,
|
|
||||||
announce: nodeAnnounceMock,
|
|
||||||
lookup: nodeLookupMock,
|
|
||||||
connect: nodeConnectMock,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let con: any
|
|
||||||
let testEnv: any
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
testEnv = await testEnvironment(logger)
|
|
||||||
con = testEnv.con
|
|
||||||
await cleanDB()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await cleanDB()
|
|
||||||
await con.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('federation', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
jest.useFakeTimers()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('call startDHT', () => {
|
|
||||||
const hashSpy = jest.spyOn(DHT, 'hash')
|
|
||||||
const keyPairSpy = jest.spyOn(DHT, 'keyPair')
|
|
||||||
beforeEach(async () => {
|
|
||||||
DHT.mockClear()
|
|
||||||
jest.clearAllMocks()
|
|
||||||
await startDHT(TEST_TOPIC)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls DHT.hash', () => {
|
|
||||||
expect(hashSpy).toBeCalledWith(Buffer.from(TEST_TOPIC))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('creates a key pair', () => {
|
|
||||||
expect(keyPairSpy).toBeCalledWith(expect.any(Buffer))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('initializes a new DHT object', () => {
|
|
||||||
expect(DHT).toBeCalledWith({ keyPair: keyPairMock })
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('DHT node', () => {
|
|
||||||
it('creates a server', () => {
|
|
||||||
expect(nodeCreateServerMock).toBeCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('listens on the server', () => {
|
|
||||||
expect(serverListenSpy).toBeCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('timers', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.runOnlyPendingTimers()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('announces on topic', () => {
|
|
||||||
expect(nodeAnnounceMock).toBeCalledWith(Buffer.from(TEST_TOPIC), keyPairMock)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('looks up on topic', () => {
|
|
||||||
expect(nodeLookupMock).toBeCalledWith(Buffer.from(TEST_TOPIC))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('server connection event', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
serverEventMocks.connection({
|
|
||||||
remotePublicKey: Buffer.from('another-public-key'),
|
|
||||||
on: socketOnMock,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can be triggered', () => {
|
|
||||||
expect(socketOnMock).toBeCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('socket events', () => {
|
|
||||||
describe('on data', () => {
|
|
||||||
describe('with receiving simply a string', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
socketEventMocks.data(Buffer.from('no-json string'))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith('data: no-json string')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs an error of unexpected data format and structure', () => {
|
|
||||||
expect(logger.error).toBeCalledWith(
|
|
||||||
'Error on receiving data from socket:',
|
|
||||||
new SyntaxError('Unexpected token o in JSON at position 1'),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving array of strings', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
const strArray: string[] = ['invalid type test', 'api', 'url']
|
|
||||||
socketEventMocks.data(Buffer.from(strArray.toString()))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith('data: invalid type test,api,url')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs an error of unexpected data format and structure', () => {
|
|
||||||
expect(logger.error).toBeCalledWith(
|
|
||||||
'Error on receiving data from socket:',
|
|
||||||
new SyntaxError('Unexpected token i in JSON at position 0'),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving array of string-arrays', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
const strArray: string[][] = [
|
|
||||||
[`api`, `url`, `invalid type in array test`],
|
|
||||||
[`wrong`, `api`, `url`],
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(strArray.toString()))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(
|
|
||||||
'data: api,url,invalid type in array test,wrong,api,url',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs an error of unexpected data format and structure', () => {
|
|
||||||
expect(logger.error).toBeCalledWith(
|
|
||||||
'Error on receiving data from socket:',
|
|
||||||
new SyntaxError('Unexpected token a in JSON at position 0'),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving JSON-Array with too much entries', () => {
|
|
||||||
let jsonArray: { api: string; url: string }[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{ api: 'v1_0', url: 'too much versions at the same time test' },
|
|
||||||
{ api: 'v1_0', url: 'url2' },
|
|
||||||
{ api: 'v1_0', url: 'url3' },
|
|
||||||
{ api: 'v1_0', url: 'url4' },
|
|
||||||
{ api: 'v1_0', url: 'url5' },
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(
|
|
||||||
'data: [{"api":"v1_0","url":"too much versions at the same time test"},{"api":"v1_0","url":"url2"},{"api":"v1_0","url":"url3"},{"api":"v1_0","url":"url4"},{"api":"v1_0","url":"url5"}]',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs a warning of too much apiVersion-Definitions', () => {
|
|
||||||
expect(logger.warn).toBeCalledWith(
|
|
||||||
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
|
|
||||||
jsonArray,
|
|
||||||
)}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving wrong but tolerated property data', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
let result: DbCommunity[] = []
|
|
||||||
beforeAll(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{
|
|
||||||
wrong: 'wrong but tolerated property test',
|
|
||||||
api: 'v1_0',
|
|
||||||
url: 'url1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'v2_0',
|
|
||||||
url: 'url2',
|
|
||||||
wrong: 'wrong but tolerated property test',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
result = await DbCommunity.find()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await cleanDB()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has two Communty entries in database', () => {
|
|
||||||
expect(result).toHaveLength(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has an entry for api version v1_0', () => {
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
publicKey: expect.any(Buffer),
|
|
||||||
apiVersion: 'v1_0',
|
|
||||||
endPoint: 'url1',
|
|
||||||
lastAnnouncedAt: expect.any(Date),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has an entry for api version v2_0', () => {
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
publicKey: expect.any(Buffer),
|
|
||||||
apiVersion: 'v2_0',
|
|
||||||
endPoint: 'url2',
|
|
||||||
lastAnnouncedAt: expect.any(Date),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data but missing api property', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{ test1: 'missing api proterty test', url: 'any url definition as string' },
|
|
||||||
{ api: 'some api', test2: 'missing url property test' },
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs a warning of invalid apiVersion-Definition', () => {
|
|
||||||
expect(logger.warn).toBeCalledWith(
|
|
||||||
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data but missing url property', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{ api: 'some api', test2: 'missing url property test' },
|
|
||||||
{ test1: 'missing api proterty test', url: 'any url definition as string' },
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs a warning of invalid apiVersion-Definition', () => {
|
|
||||||
expect(logger.warn).toBeCalledWith(
|
|
||||||
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data but wrong type of api property', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{ api: 1, url: 'wrong property type tests' },
|
|
||||||
{ api: 'urltyptest', url: 2 },
|
|
||||||
{ api: 1, url: 2 },
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs a warning of invalid apiVersion-Definition', () => {
|
|
||||||
expect(logger.warn).toBeCalledWith(
|
|
||||||
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data but wrong type of url property', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{ api: 'urltyptest', url: 2 },
|
|
||||||
{ api: 1, url: 'wrong property type tests' },
|
|
||||||
{ api: 1, url: 2 },
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs a warning of invalid apiVersion-Definition', () => {
|
|
||||||
expect(logger.warn).toBeCalledWith(
|
|
||||||
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data but wrong type of both properties', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{ api: 1, url: 2 },
|
|
||||||
{ api: 'urltyptest', url: 2 },
|
|
||||||
{ api: 1, url: 'wrong property type tests' },
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs a warning of invalid apiVersion-Definition', () => {
|
|
||||||
expect(logger.warn).toBeCalledWith(
|
|
||||||
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data but too long api string', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{ api: 'toolong api', url: 'some valid url' },
|
|
||||||
{
|
|
||||||
api: 'valid api',
|
|
||||||
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'toolong api',
|
|
||||||
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs a warning of invalid apiVersion-Definition', () => {
|
|
||||||
expect(logger.warn).toBeCalledWith(
|
|
||||||
`received apiVersion with content longer than max length: ${JSON.stringify(
|
|
||||||
jsonArray[0],
|
|
||||||
)}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data but too long url string', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{
|
|
||||||
api: 'api',
|
|
||||||
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
|
||||||
},
|
|
||||||
{ api: 'toolong api', url: 'some valid url' },
|
|
||||||
{
|
|
||||||
api: 'toolong api',
|
|
||||||
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs a warning of invalid apiVersion-Definition', () => {
|
|
||||||
expect(logger.warn).toBeCalledWith(
|
|
||||||
`received apiVersion with content longer than max length: ${JSON.stringify(
|
|
||||||
jsonArray[0],
|
|
||||||
)}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data but both properties with too long strings', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{
|
|
||||||
api: 'toolong api',
|
|
||||||
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'api',
|
|
||||||
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
|
||||||
},
|
|
||||||
{ api: 'toolong api', url: 'some valid url' },
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data of exact max allowed properties length', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
let result: DbCommunity[] = []
|
|
||||||
beforeAll(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{
|
|
||||||
api: 'valid api',
|
|
||||||
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'api',
|
|
||||||
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
|
||||||
},
|
|
||||||
{ api: 'toolong api', url: 'some valid url' },
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
result = await DbCommunity.find()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await cleanDB()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has one Communty entry in database', () => {
|
|
||||||
expect(result).toHaveLength(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`has an entry with max content length for api and url`, () => {
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
publicKey: expect.any(Buffer),
|
|
||||||
apiVersion: 'valid api',
|
|
||||||
endPoint:
|
|
||||||
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
lastAnnouncedAt: expect.any(Date),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data of exact max allowed buffer length', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
let result: DbCommunity[] = []
|
|
||||||
beforeAll(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{
|
|
||||||
api: 'valid api1',
|
|
||||||
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'valid api2',
|
|
||||||
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'valid api3',
|
|
||||||
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'valid api4',
|
|
||||||
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
result = await DbCommunity.find()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await cleanDB()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has five Communty entries in database', () => {
|
|
||||||
expect(result).toHaveLength(4)
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`has an entry 'valid api1' with max content length for api and url`, () => {
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
publicKey: expect.any(Buffer),
|
|
||||||
apiVersion: 'valid api1',
|
|
||||||
endPoint:
|
|
||||||
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
lastAnnouncedAt: expect.any(Date),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`has an entry 'valid api2' with max content length for api and url`, () => {
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
publicKey: expect.any(Buffer),
|
|
||||||
apiVersion: 'valid api2',
|
|
||||||
endPoint:
|
|
||||||
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
lastAnnouncedAt: expect.any(Date),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`has an entry 'valid api3' with max content length for api and url`, () => {
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
publicKey: expect.any(Buffer),
|
|
||||||
apiVersion: 'valid api3',
|
|
||||||
endPoint:
|
|
||||||
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
lastAnnouncedAt: expect.any(Date),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`has an entry 'valid api4' with max content length for api and url`, () => {
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
publicKey: expect.any(Buffer),
|
|
||||||
apiVersion: 'valid api4',
|
|
||||||
endPoint:
|
|
||||||
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
lastAnnouncedAt: expect.any(Date),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with receiving data longer than max allowed buffer length', () => {
|
|
||||||
let jsonArray: any[]
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
jsonArray = [
|
|
||||||
{
|
|
||||||
api: 'Xvalid api1',
|
|
||||||
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'valid api2',
|
|
||||||
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'valid api3',
|
|
||||||
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'valid api4',
|
|
||||||
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the received data', () => {
|
|
||||||
expect(logger.warn).toBeCalledWith(
|
|
||||||
`received more than max allowed length of data buffer: ${
|
|
||||||
JSON.stringify(jsonArray).length
|
|
||||||
} against 1141 max allowed`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with proper data', () => {
|
|
||||||
let result: DbCommunity[] = []
|
|
||||||
beforeAll(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
await socketEventMocks.data(
|
|
||||||
Buffer.from(
|
|
||||||
JSON.stringify([
|
|
||||||
{
|
|
||||||
api: 'v1_0',
|
|
||||||
url: 'http://localhost:4000/api/v1_0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'v2_0',
|
|
||||||
url: 'http://localhost:4000/api/v2_0',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
result = await DbCommunity.find()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await cleanDB()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has two Communty entries in database', () => {
|
|
||||||
expect(result).toHaveLength(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has an entry for api version v1_0', () => {
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
publicKey: expect.any(Buffer),
|
|
||||||
apiVersion: 'v1_0',
|
|
||||||
endPoint: 'http://localhost:4000/api/v1_0',
|
|
||||||
lastAnnouncedAt: expect.any(Date),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has an entry for api version v2_0', () => {
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
publicKey: expect.any(Buffer),
|
|
||||||
apiVersion: 'v2_0',
|
|
||||||
endPoint: 'http://localhost:4000/api/v2_0',
|
|
||||||
lastAnnouncedAt: expect.any(Date),
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('on open', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
socketEventMocks.open()
|
|
||||||
})
|
|
||||||
|
|
||||||
it.skip('calls socket write with own api versions', () => {
|
|
||||||
expect(socketWriteMock).toBeCalledWith(
|
|
||||||
Buffer.from(
|
|
||||||
JSON.stringify([
|
|
||||||
{
|
|
||||||
api: 'v1_0',
|
|
||||||
url: 'http://localhost:4000/api/v1_0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'v1_1',
|
|
||||||
url: 'http://localhost:4000/api/v1_1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: 'v2_0',
|
|
||||||
url: 'http://localhost:4000/api/v2_0',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
import DHT from '@hyperswarm/dht'
|
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
|
||||||
import CONFIG from '@/config'
|
|
||||||
import { Community as DbCommunity } from '@entity/Community'
|
|
||||||
|
|
||||||
const KEY_SECRET_SEEDBYTES = 32
|
|
||||||
const getSeed = (): Buffer | null =>
|
|
||||||
CONFIG.FEDERATION_DHT_SEED ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) : null
|
|
||||||
|
|
||||||
const POLLTIME = 20000
|
|
||||||
const SUCCESSTIME = 120000
|
|
||||||
const ERRORTIME = 240000
|
|
||||||
const ANNOUNCETIME = 30000
|
|
||||||
|
|
||||||
enum ApiVersionType {
|
|
||||||
V1_0 = 'v1_0',
|
|
||||||
V1_1 = 'v1_1',
|
|
||||||
V2_0 = 'v2_0',
|
|
||||||
}
|
|
||||||
type CommunityApi = {
|
|
||||||
api: string
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const startDHT = async (topic: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const TOPIC = DHT.hash(Buffer.from(topic))
|
|
||||||
const keyPair = DHT.keyPair(getSeed())
|
|
||||||
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
|
|
||||||
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
|
|
||||||
|
|
||||||
const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) {
|
|
||||||
const comApi: CommunityApi = {
|
|
||||||
api: apiEnum,
|
|
||||||
url: CONFIG.FEDERATION_COMMUNITY_URL + apiEnum,
|
|
||||||
}
|
|
||||||
return comApi
|
|
||||||
})
|
|
||||||
logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
|
|
||||||
|
|
||||||
const node = new DHT({ keyPair })
|
|
||||||
|
|
||||||
const server = node.createServer()
|
|
||||||
|
|
||||||
server.on('connection', function (socket: any) {
|
|
||||||
logger.info(`server on... with Remote public key: ${socket.remotePublicKey.toString('hex')}`)
|
|
||||||
|
|
||||||
socket.on('data', async (data: Buffer) => {
|
|
||||||
try {
|
|
||||||
if (data.length > 1141) {
|
|
||||||
logger.warn(
|
|
||||||
`received more than max allowed length of data buffer: ${data.length} against 1141 max allowed`,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.info(`data: ${data.toString('ascii')}`)
|
|
||||||
const recApiVersions: CommunityApi[] = JSON.parse(data.toString('ascii'))
|
|
||||||
|
|
||||||
// TODO better to introduce the validation by https://github.com/typestack/class-validato
|
|
||||||
if (recApiVersions && Array.isArray(recApiVersions) && recApiVersions.length < 5) {
|
|
||||||
for (const recApiVersion of recApiVersions) {
|
|
||||||
if (
|
|
||||||
!recApiVersion.api ||
|
|
||||||
typeof recApiVersion.api !== 'string' ||
|
|
||||||
!recApiVersion.url ||
|
|
||||||
typeof recApiVersion.url !== 'string'
|
|
||||||
) {
|
|
||||||
logger.warn(
|
|
||||||
`received invalid apiVersion-Definition: ${JSON.stringify(recApiVersion)}`,
|
|
||||||
)
|
|
||||||
// in a forEach-loop use return instead of continue
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO better to introduce the validation on entity-Level by https://github.com/typestack/class-validator
|
|
||||||
if (recApiVersion.api.length > 10 || recApiVersion.url.length > 255) {
|
|
||||||
logger.warn(
|
|
||||||
`received apiVersion with content longer than max length: ${JSON.stringify(
|
|
||||||
recApiVersion,
|
|
||||||
)}`,
|
|
||||||
)
|
|
||||||
// in a forEach-loop use return instead of continue
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
apiVersion: recApiVersion.api,
|
|
||||||
endPoint: recApiVersion.url,
|
|
||||||
publicKey: socket.remotePublicKey.toString('hex'),
|
|
||||||
lastAnnouncedAt: new Date(),
|
|
||||||
}
|
|
||||||
logger.debug(`upsert with variables=${JSON.stringify(variables)}`)
|
|
||||||
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
|
|
||||||
await DbCommunity.createQueryBuilder()
|
|
||||||
.insert()
|
|
||||||
.into(DbCommunity)
|
|
||||||
.values(variables)
|
|
||||||
.orUpdate({
|
|
||||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
|
||||||
overwrite: ['end_point', 'last_announced_at'],
|
|
||||||
})
|
|
||||||
.execute()
|
|
||||||
logger.info(`federation community upserted successfully...`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
|
|
||||||
recApiVersions,
|
|
||||||
)}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Error on receiving data from socket:', e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await server.listen()
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
logger.info(`Announcing on topic: ${TOPIC.toString('hex')}`)
|
|
||||||
await node.announce(TOPIC, keyPair).finished()
|
|
||||||
}, ANNOUNCETIME)
|
|
||||||
|
|
||||||
let successfulRequests: string[] = []
|
|
||||||
let errorfulRequests: string[] = []
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
logger.info('Refreshing successful nodes')
|
|
||||||
successfulRequests = []
|
|
||||||
}, SUCCESSTIME)
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
logger.info('Refreshing errorful nodes')
|
|
||||||
errorfulRequests = []
|
|
||||||
}, ERRORTIME)
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
const result = await node.lookup(TOPIC)
|
|
||||||
|
|
||||||
const collectedPubKeys: string[] = []
|
|
||||||
|
|
||||||
for await (const data of result) {
|
|
||||||
data.peers.forEach((peer: any) => {
|
|
||||||
const pubKey = peer.publicKey.toString('hex')
|
|
||||||
if (
|
|
||||||
pubKey !== keyPair.publicKey.toString('hex') &&
|
|
||||||
!successfulRequests.includes(pubKey) &&
|
|
||||||
!errorfulRequests.includes(pubKey) &&
|
|
||||||
!collectedPubKeys.includes(pubKey)
|
|
||||||
) {
|
|
||||||
collectedPubKeys.push(peer.publicKey.toString('hex'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Found new peers: ${collectedPubKeys}`)
|
|
||||||
|
|
||||||
collectedPubKeys.forEach((remotePubKey) => {
|
|
||||||
const socket = node.connect(Buffer.from(remotePubKey, 'hex'))
|
|
||||||
|
|
||||||
// socket.once("connect", function () {
|
|
||||||
// console.log("client side emitted connect");
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socket.once("end", function () {
|
|
||||||
// console.log("client side ended");
|
|
||||||
// });
|
|
||||||
|
|
||||||
socket.once('error', (err: any) => {
|
|
||||||
errorfulRequests.push(remotePubKey)
|
|
||||||
logger.error(`error on peer ${remotePubKey}: ${err.message}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('open', function () {
|
|
||||||
socket.write(Buffer.from(JSON.stringify(ownApiVersions)))
|
|
||||||
successfulRequests.push(remotePubKey)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, POLLTIME)
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('DHT unexpected error:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { registerEnumType } from 'type-graphql'
|
|
||||||
|
|
||||||
export enum TransactionType {
|
|
||||||
CREATION = 'creation',
|
|
||||||
SEND = 'send',
|
|
||||||
RECIEVE = 'receive',
|
|
||||||
}
|
|
||||||
|
|
||||||
registerEnumType(TransactionType, {
|
|
||||||
name: 'TransactionType', // this one is mandatory
|
|
||||||
description: 'Name of the Type of the transaction', // this one is optional
|
|
||||||
})
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import createServer from './server/createServer'
|
import createServer from './server/createServer'
|
||||||
import { startDHT } from '@/federation/index'
|
|
||||||
|
|
||||||
// config
|
// config
|
||||||
import CONFIG from './config'
|
import CONFIG from './config'
|
||||||
@ -17,20 +16,6 @@ async function main() {
|
|||||||
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
|
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// start DHT hyperswarm when DHT_TOPIC is set in .env
|
|
||||||
if (CONFIG.FEDERATION_DHT_TOPIC) {
|
|
||||||
if (CONFIG.FEDERATION_COMMUNITY_URL === null) {
|
|
||||||
throw Error(`Config-Error: missing configuration of property FEDERATION_COMMUNITY_URL`)
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(
|
|
||||||
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
|
||||||
CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...'
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
await startDHT(CONFIG.FEDERATION_DHT_TOPIC) // con,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => {
|
main().catch((e) => {
|
||||||
|
|||||||
3
federation/.eslintignore
Normal file
3
federation/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
**/*.min.js
|
||||||
|
build
|
||||||
28
federation/.eslintrc.js
Normal file
28
federation/.eslintrc.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
// jest: true,
|
||||||
|
},
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['prettier', '@typescript-eslint' /*, 'jest' */],
|
||||||
|
extends: [
|
||||||
|
'standard',
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
],
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {
|
||||||
|
'no-console': ['error'],
|
||||||
|
'no-debugger': 'error',
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
htmlWhitespaceSensitivity: 'ignore',
|
||||||
|
semi: false,
|
||||||
|
singleQuote: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
8
federation/.gitignore
vendored
Normal file
8
federation/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/node_modules/
|
||||||
|
/.env
|
||||||
|
/.env.bak
|
||||||
|
/build/
|
||||||
|
package-json.lock
|
||||||
|
coverage
|
||||||
|
# emacs
|
||||||
|
*~
|
||||||
146
federation/log4js-config.json
Normal file
146
federation/log4js-config.json
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
{
|
||||||
|
"appenders":
|
||||||
|
{
|
||||||
|
"access":
|
||||||
|
{
|
||||||
|
"type": "dateFile",
|
||||||
|
"filename": "../logs/federation/access-%p.log",
|
||||||
|
"pattern": "yyyy-MM-dd",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||||
|
},
|
||||||
|
"keepFileExt" : true,
|
||||||
|
"fileNameSep" : "_",
|
||||||
|
"numBackups" : 30
|
||||||
|
},
|
||||||
|
"apollo":
|
||||||
|
{
|
||||||
|
"type": "dateFile",
|
||||||
|
"filename": "../logs/federation/apollo-%p.log",
|
||||||
|
"pattern": "yyyy-MM-dd",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||||
|
},
|
||||||
|
"keepFileExt" : true,
|
||||||
|
"fileNameSep" : "_",
|
||||||
|
"numBackups" : 30
|
||||||
|
},
|
||||||
|
"backend":
|
||||||
|
{
|
||||||
|
"type": "dateFile",
|
||||||
|
"filename": "../logs/federation/backend-%p.log",
|
||||||
|
"pattern": "yyyy-MM-dd",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||||
|
},
|
||||||
|
"keepFileExt" : true,
|
||||||
|
"fileNameSep" : "_",
|
||||||
|
"numBackups" : 30
|
||||||
|
},
|
||||||
|
"federation":
|
||||||
|
{
|
||||||
|
"type": "dateFile",
|
||||||
|
"filename": "../logs/federation/apiversion-%v-%p.log",
|
||||||
|
"pattern": "yyyy-MM-dd",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||||
|
},
|
||||||
|
"keepFileExt" : true,
|
||||||
|
"fileNameSep" : "_",
|
||||||
|
"numBackups" : 30
|
||||||
|
},
|
||||||
|
"errorFile":
|
||||||
|
{
|
||||||
|
"type": "dateFile",
|
||||||
|
"filename": "../logs/federation/errors-%p.log",
|
||||||
|
"pattern": "yyyy-MM-dd",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||||
|
},
|
||||||
|
"keepFileExt" : true,
|
||||||
|
"fileNameSep" : "_",
|
||||||
|
"numBackups" : 30
|
||||||
|
},
|
||||||
|
"errors":
|
||||||
|
{
|
||||||
|
"type": "logLevelFilter",
|
||||||
|
"level": "error",
|
||||||
|
"appender": "errorFile"
|
||||||
|
},
|
||||||
|
"out":
|
||||||
|
{
|
||||||
|
"type": "stdout",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"apolloOut":
|
||||||
|
{
|
||||||
|
"type": "stdout",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"categories":
|
||||||
|
{
|
||||||
|
"default":
|
||||||
|
{
|
||||||
|
"appenders":
|
||||||
|
[
|
||||||
|
"out",
|
||||||
|
"errors"
|
||||||
|
],
|
||||||
|
"level": "debug",
|
||||||
|
"enableCallStack": true
|
||||||
|
},
|
||||||
|
"apollo":
|
||||||
|
{
|
||||||
|
"appenders":
|
||||||
|
[
|
||||||
|
"apollo",
|
||||||
|
"apolloOut",
|
||||||
|
"errors"
|
||||||
|
],
|
||||||
|
"level": "debug",
|
||||||
|
"enableCallStack": true
|
||||||
|
},
|
||||||
|
"backend":
|
||||||
|
{
|
||||||
|
"appenders":
|
||||||
|
[
|
||||||
|
"backend",
|
||||||
|
"out",
|
||||||
|
"errors"
|
||||||
|
],
|
||||||
|
"level": "debug",
|
||||||
|
"enableCallStack": true
|
||||||
|
},
|
||||||
|
"federation":
|
||||||
|
{
|
||||||
|
"appenders":
|
||||||
|
[
|
||||||
|
"federation",
|
||||||
|
"out",
|
||||||
|
"errors"
|
||||||
|
],
|
||||||
|
"level": "debug",
|
||||||
|
"enableCallStack": true
|
||||||
|
},
|
||||||
|
"http":
|
||||||
|
{
|
||||||
|
"appenders":
|
||||||
|
[
|
||||||
|
"access"
|
||||||
|
],
|
||||||
|
"level": "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
federation/package.json
Normal file
49
federation/package.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "gradido-federation",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"repository": "https://github.com/gradido/gradido/federation",
|
||||||
|
"author": "Claus-Peter Huebner",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"private": false,
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build",
|
||||||
|
"clean": "tsc --build --clean",
|
||||||
|
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
|
||||||
|
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts",
|
||||||
|
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"apollo-server-express": "^2.25.2",
|
||||||
|
"class-validator": "^0.13.2",
|
||||||
|
"cors": "2.8.5",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"dotenv": "10.0.0",
|
||||||
|
"express": "4.17.1",
|
||||||
|
"graphql": "15.5.1",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"log4js": "^6.7.1",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.1.1",
|
||||||
|
"type-graphql": "^1.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||||
|
"@typescript-eslint/parser": "^4.28.0",
|
||||||
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
|
"@types/node": "^16.10.3",
|
||||||
|
"eslint": "^7.29.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-config-standard": "^16.0.3",
|
||||||
|
"eslint-plugin-import": "^2.23.4",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
|
"prettier": "^2.3.1",
|
||||||
|
"typescript": "^4.3.4",
|
||||||
|
"nodemon": "^2.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
94
federation/src/config/index.ts
Normal file
94
federation/src/config/index.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
dotenv.config()
|
||||||
|
/*
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
|
Decimal.set({
|
||||||
|
precision: 25,
|
||||||
|
rounding: Decimal.ROUND_HALF_UP,
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
|
const constants = {
|
||||||
|
DB_VERSION: '0059-add_hide_amount_to_users',
|
||||||
|
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||||
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
|
// default log level on production should be info
|
||||||
|
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||||
|
CONFIG_VERSION: {
|
||||||
|
DEFAULT: 'DEFAULT',
|
||||||
|
EXPECTED: 'v1.2023-01-09',
|
||||||
|
CURRENT: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = {
|
||||||
|
PORT: process.env.PORT || 5000,
|
||||||
|
// JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||||
|
// JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
|
||||||
|
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||||
|
// GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
|
||||||
|
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||||
|
}
|
||||||
|
const database = {
|
||||||
|
DB_HOST: process.env.DB_HOST || 'localhost',
|
||||||
|
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
|
||||||
|
DB_USER: process.env.DB_USER || 'root',
|
||||||
|
DB_PASSWORD: process.env.DB_PASSWORD || '',
|
||||||
|
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
|
||||||
|
TYPEORM_LOGGING_RELATIVE_PATH:
|
||||||
|
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
const community = {
|
||||||
|
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
|
||||||
|
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
|
||||||
|
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
|
||||||
|
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
|
||||||
|
COMMUNITY_REDEEM_CONTRIBUTION_URL:
|
||||||
|
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
|
||||||
|
COMMUNITY_DESCRIPTION:
|
||||||
|
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// const eventProtocol = {
|
||||||
|
// global switch to enable writing of EventProtocol-Entries
|
||||||
|
// EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// This is needed by graphql-directive-auth
|
||||||
|
// process.env.APP_SECRET = server.JWT_SECRET
|
||||||
|
|
||||||
|
// Check config version
|
||||||
|
constants.CONFIG_VERSION.CURRENT =
|
||||||
|
process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
constants.CONFIG_VERSION.EXPECTED,
|
||||||
|
constants.CONFIG_VERSION.DEFAULT,
|
||||||
|
].includes(constants.CONFIG_VERSION.CURRENT)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const federation = {
|
||||||
|
// FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
|
||||||
|
// FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
|
||||||
|
FEDERATION_PORT: process.env.FEDERATION_PORT || 5000,
|
||||||
|
FEDERATION_API: process.env.FEDERATION_API || '1_0',
|
||||||
|
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
...constants,
|
||||||
|
...server,
|
||||||
|
...database,
|
||||||
|
// ...community,
|
||||||
|
// ...eventProtocol,
|
||||||
|
...federation,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CONFIG
|
||||||
12
federation/src/graphql/api/1_0/resolver/Test2Resolver.ts
Normal file
12
federation/src/graphql/api/1_0/resolver/Test2Resolver.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Query, Resolver } from 'type-graphql'
|
||||||
|
import { federationLogger as logger } from '@/server/logger'
|
||||||
|
import { GetTestApiResult } from '../../GetTestApiResult'
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class Test2Resolver {
|
||||||
|
@Query(() => GetTestApiResult)
|
||||||
|
async test2(): Promise<GetTestApiResult> {
|
||||||
|
logger.info(`test api 2 1_0`)
|
||||||
|
return new GetTestApiResult('1_0')
|
||||||
|
}
|
||||||
|
}
|
||||||
12
federation/src/graphql/api/1_0/resolver/TestResolver.ts
Normal file
12
federation/src/graphql/api/1_0/resolver/TestResolver.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
|
||||||
|
import { federationLogger as logger } from '@/server/logger'
|
||||||
|
import { GetTestApiResult } from '../../GetTestApiResult'
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class TestResolver {
|
||||||
|
@Query(() => GetTestApiResult)
|
||||||
|
async test(): Promise<GetTestApiResult> {
|
||||||
|
logger.info(`test api 1_0`)
|
||||||
|
return new GetTestApiResult('1_0')
|
||||||
|
}
|
||||||
|
}
|
||||||
12
federation/src/graphql/api/1_1/resolver/TestResolver.ts
Normal file
12
federation/src/graphql/api/1_1/resolver/TestResolver.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
|
||||||
|
import { federationLogger as logger } from '@/server/logger'
|
||||||
|
import { GetTestApiResult } from '../../GetTestApiResult'
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class TestResolver {
|
||||||
|
@Query(() => GetTestApiResult)
|
||||||
|
async test(): Promise<GetTestApiResult> {
|
||||||
|
logger.info(`test api 1_1`)
|
||||||
|
return new GetTestApiResult('1_1')
|
||||||
|
}
|
||||||
|
}
|
||||||
12
federation/src/graphql/api/2_0/resolver/TestResolver.ts
Normal file
12
federation/src/graphql/api/2_0/resolver/TestResolver.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
|
||||||
|
import { federationLogger as logger } from '@/server/logger'
|
||||||
|
import { GetTestApiResult } from '../../GetTestApiResult'
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class TestResolver {
|
||||||
|
@Query(() => GetTestApiResult)
|
||||||
|
async test(): Promise<GetTestApiResult> {
|
||||||
|
logger.info(`test api 2_0`)
|
||||||
|
return new GetTestApiResult('2_0')
|
||||||
|
}
|
||||||
|
}
|
||||||
11
federation/src/graphql/api/GetTestApiResult.ts
Normal file
11
federation/src/graphql/api/GetTestApiResult.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Field, ObjectType } from 'type-graphql'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class GetTestApiResult {
|
||||||
|
constructor(apiVersion: string) {
|
||||||
|
this.api = apiVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
api: string
|
||||||
|
}
|
||||||
12
federation/src/graphql/api/schema.ts
Normal file
12
federation/src/graphql/api/schema.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import path from 'path'
|
||||||
|
// config
|
||||||
|
import CONFIG from '../../config'
|
||||||
|
import { federationLogger as logger } from '@/server/logger'
|
||||||
|
|
||||||
|
export const getApiResolvers = (): string => {
|
||||||
|
logger.info(`getApiResolvers...${CONFIG.FEDERATION_API}`)
|
||||||
|
return path.join(
|
||||||
|
__dirname,
|
||||||
|
`./${CONFIG.FEDERATION_API}/resolver/*Resolver.{ts,js}`
|
||||||
|
)
|
||||||
|
}
|
||||||
23
federation/src/graphql/scalar/Decimal.ts
Normal file
23
federation/src/graphql/scalar/Decimal.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { GraphQLScalarType, Kind } from 'graphql'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
|
export default new GraphQLScalarType({
|
||||||
|
name: 'Decimal',
|
||||||
|
description: 'The `Decimal` scalar type to represent currency values',
|
||||||
|
|
||||||
|
serialize(value: Decimal) {
|
||||||
|
return value.toString()
|
||||||
|
},
|
||||||
|
|
||||||
|
parseValue(value) {
|
||||||
|
return new Decimal(value)
|
||||||
|
},
|
||||||
|
|
||||||
|
parseLiteral(ast) {
|
||||||
|
if (ast.kind !== Kind.STRING) {
|
||||||
|
throw new TypeError(`${String(ast)} is not a valid decimal value.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Decimal(ast.value)
|
||||||
|
},
|
||||||
|
})
|
||||||
17
federation/src/graphql/schema.ts
Normal file
17
federation/src/graphql/schema.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { GraphQLSchema } from 'graphql'
|
||||||
|
import { buildSchema } from 'type-graphql'
|
||||||
|
|
||||||
|
// import isAuthorized from './directive/isAuthorized'
|
||||||
|
import DecimalScalar from './scalar/Decimal'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { getApiResolvers } from './api/schema'
|
||||||
|
|
||||||
|
const schema = async (): Promise<GraphQLSchema> => {
|
||||||
|
return await buildSchema({
|
||||||
|
resolvers: [getApiResolvers()],
|
||||||
|
// authChecker: isAuthorized,
|
||||||
|
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default schema
|
||||||
33
federation/src/index.ts
Normal file
33
federation/src/index.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import createServer from './server/createServer'
|
||||||
|
|
||||||
|
// config
|
||||||
|
import CONFIG from './config'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`FEDERATION_PORT=${CONFIG.FEDERATION_PORT}`)
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`FEDERATION_API=${CONFIG.FEDERATION_API}`)
|
||||||
|
const { app } = await createServer()
|
||||||
|
|
||||||
|
app.listen(CONFIG.FEDERATION_PORT, () => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`Server is running at http://localhost:${CONFIG.FEDERATION_PORT}`
|
||||||
|
)
|
||||||
|
if (CONFIG.GRAPHIQL) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`GraphIQL available at http://localhost:${CONFIG.FEDERATION_PORT}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
8
federation/src/server/cors.ts
Normal file
8
federation/src/server/cors.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import cors from 'cors'
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin: '*',
|
||||||
|
exposedHeaders: ['token'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default cors(corsOptions)
|
||||||
91
federation/src/server/createServer.ts
Normal file
91
federation/src/server/createServer.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import 'reflect-metadata'
|
||||||
|
|
||||||
|
import { ApolloServer } from 'apollo-server-express'
|
||||||
|
import express, { Express } from 'express'
|
||||||
|
|
||||||
|
// database
|
||||||
|
import connection from '@/typeorm/connection'
|
||||||
|
import { checkDBVersion } from '@/typeorm/DBVersion'
|
||||||
|
|
||||||
|
// server
|
||||||
|
import cors from './cors'
|
||||||
|
// import serverContext from './context'
|
||||||
|
import plugins from './plugins'
|
||||||
|
|
||||||
|
// config
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
|
// graphql
|
||||||
|
import schema from '@/graphql/schema'
|
||||||
|
|
||||||
|
// webhooks
|
||||||
|
// import { elopageWebhook } from '@/webhook/elopage'
|
||||||
|
import { Connection } from '@dbTools/typeorm'
|
||||||
|
|
||||||
|
import { apolloLogger } from './logger'
|
||||||
|
import { Logger } from 'log4js'
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
// import { i18n } from './localization'
|
||||||
|
|
||||||
|
// TODO implement
|
||||||
|
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
||||||
|
|
||||||
|
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
|
||||||
|
|
||||||
|
const createServer = async (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
// context: any = serverContext,
|
||||||
|
logger: Logger = apolloLogger
|
||||||
|
// localization: i18n.I18n = i18n,
|
||||||
|
): Promise<ServerDef> => {
|
||||||
|
logger.addContext('user', 'unknown')
|
||||||
|
logger.debug('createServer...')
|
||||||
|
|
||||||
|
// open mysql connection
|
||||||
|
const con = await connection()
|
||||||
|
if (!con || !con.isConnected) {
|
||||||
|
logger.fatal(`Couldn't open connection to database!`)
|
||||||
|
throw new Error(`Fatal: Couldn't open connection to database`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for correct database version
|
||||||
|
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
|
||||||
|
if (!dbVersion) {
|
||||||
|
logger.fatal('Fatal: Database Version incorrect')
|
||||||
|
throw new Error('Fatal: Database Version incorrect')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Express Server
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
// cors
|
||||||
|
app.use(cors)
|
||||||
|
|
||||||
|
// bodyparser json
|
||||||
|
app.use(express.json())
|
||||||
|
// bodyparser urlencoded for elopage
|
||||||
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
// app.use(localization.init)
|
||||||
|
|
||||||
|
// Elopage Webhook
|
||||||
|
// app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
||||||
|
|
||||||
|
// Apollo Server
|
||||||
|
const apollo = new ApolloServer({
|
||||||
|
schema: await schema(),
|
||||||
|
// playground: CONFIG.GRAPHIQL,
|
||||||
|
// introspection: CONFIG.GRAPHIQL,
|
||||||
|
// context,
|
||||||
|
plugins,
|
||||||
|
logger,
|
||||||
|
})
|
||||||
|
apollo.applyMiddleware({ app, path: '/' })
|
||||||
|
logger.debug('createServer...successful')
|
||||||
|
|
||||||
|
return { apollo, app, con }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createServer
|
||||||
43
federation/src/server/logger.ts
Normal file
43
federation/src/server/logger.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import log4js from 'log4js'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
|
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
|
||||||
|
|
||||||
|
options.categories.backend.level = CONFIG.LOG_LEVEL
|
||||||
|
options.categories.apollo.level = CONFIG.LOG_LEVEL
|
||||||
|
let filename: string = options.appenders.federation.filename
|
||||||
|
options.appenders.federation.filename = filename
|
||||||
|
.replace('%v', CONFIG.FEDERATION_API)
|
||||||
|
.replace('%p', CONFIG.FEDERATION_PORT.toString())
|
||||||
|
filename = options.appenders.access.filename
|
||||||
|
options.appenders.access.filename = filename.replace(
|
||||||
|
'%p',
|
||||||
|
CONFIG.FEDERATION_PORT.toString()
|
||||||
|
)
|
||||||
|
filename = options.appenders.apollo.filename
|
||||||
|
options.appenders.apollo.filename = filename.replace(
|
||||||
|
'%p',
|
||||||
|
CONFIG.FEDERATION_PORT.toString()
|
||||||
|
)
|
||||||
|
filename = options.appenders.backend.filename
|
||||||
|
options.appenders.backend.filename = filename.replace(
|
||||||
|
'%p',
|
||||||
|
CONFIG.FEDERATION_PORT.toString()
|
||||||
|
)
|
||||||
|
filename = options.appenders.errorFile.filename
|
||||||
|
options.appenders.errorFile.filename = filename.replace(
|
||||||
|
'%p',
|
||||||
|
CONFIG.FEDERATION_PORT.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
log4js.configure(options)
|
||||||
|
|
||||||
|
const apolloLogger = log4js.getLogger('apollo')
|
||||||
|
// const backendLogger = log4js.getLogger('backend')
|
||||||
|
const federationLogger = log4js.getLogger('federation')
|
||||||
|
|
||||||
|
// backendLogger.addContext('user', 'unknown')
|
||||||
|
|
||||||
|
export { apolloLogger, federationLogger }
|
||||||
68
federation/src/server/plugins.ts
Normal file
68
federation/src/server/plugins.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import clonedeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
const setHeadersPlugin = {
|
||||||
|
requestDidStart() {
|
||||||
|
return {
|
||||||
|
willSendResponse(requestContext: any) {
|
||||||
|
const { setHeaders = [] } = requestContext.context
|
||||||
|
setHeaders.forEach(({ key, value }: { [key: string]: string }) => {
|
||||||
|
if (requestContext.response.http.headers.get(key)) {
|
||||||
|
requestContext.response.http.headers.set(key, value)
|
||||||
|
} else {
|
||||||
|
requestContext.response.http.headers.append(key, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return requestContext
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterVariables = (variables: any) => {
|
||||||
|
const vars = clonedeep(variables)
|
||||||
|
if (vars.password) vars.password = '***'
|
||||||
|
if (vars.passwordNew) vars.passwordNew = '***'
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
const logPlugin = {
|
||||||
|
requestDidStart(requestContext: any) {
|
||||||
|
const { logger } = requestContext
|
||||||
|
const { query, mutation, variables, operationName } = requestContext.request
|
||||||
|
if (operationName !== 'IntrospectionQuery') {
|
||||||
|
logger.info(`Request:
|
||||||
|
${mutation || query}variables: ${JSON.stringify(
|
||||||
|
filterVariables(variables),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
willSendResponse(requestContext: any) {
|
||||||
|
if (operationName !== 'IntrospectionQuery') {
|
||||||
|
if (requestContext.context.user)
|
||||||
|
logger.info(`User ID: ${requestContext.context.user.id}`)
|
||||||
|
if (requestContext.response.data) {
|
||||||
|
logger.info('Response Success!')
|
||||||
|
logger.trace(`Response-Data:
|
||||||
|
${JSON.stringify(requestContext.response.data, null, 2)}`)
|
||||||
|
}
|
||||||
|
if (requestContext.response.errors)
|
||||||
|
logger.error(`Response-Errors:
|
||||||
|
${JSON.stringify(requestContext.response.errors, null, 2)}`)
|
||||||
|
}
|
||||||
|
return requestContext
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugins =
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? [setHeadersPlugin]
|
||||||
|
: [setHeadersPlugin, logPlugin]
|
||||||
|
|
||||||
|
export default plugins
|
||||||
27
federation/src/typeorm/DBVersion.ts
Normal file
27
federation/src/typeorm/DBVersion.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Migration } from '@entity/Migration'
|
||||||
|
import { federationLogger as logger } from '@/server/logger'
|
||||||
|
|
||||||
|
const getDBVersion = async (): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
|
||||||
|
return dbVersion ? dbVersion.fileName : null
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
||||||
|
const dbVersion = await getDBVersion()
|
||||||
|
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
|
||||||
|
logger.error(
|
||||||
|
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
|
||||||
|
dbVersion || 'None'
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkDBVersion, getDBVersion }
|
||||||
34
federation/src/typeorm/connection.ts
Normal file
34
federation/src/typeorm/connection.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// TODO This is super weird - since the entities are defined in another project they have their own globals.
|
||||||
|
// We cannot use our connection here, but must use the external typeorm installation
|
||||||
|
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import { entities } from '@entity/index'
|
||||||
|
|
||||||
|
const connection = async (): Promise<Connection | null> => {
|
||||||
|
try {
|
||||||
|
return createConnection({
|
||||||
|
name: 'default',
|
||||||
|
type: 'mysql',
|
||||||
|
host: CONFIG.DB_HOST,
|
||||||
|
port: CONFIG.DB_PORT,
|
||||||
|
username: CONFIG.DB_USER,
|
||||||
|
password: CONFIG.DB_PASSWORD,
|
||||||
|
database: CONFIG.DB_DATABASE,
|
||||||
|
entities,
|
||||||
|
synchronize: false,
|
||||||
|
logging: true,
|
||||||
|
logger: new FileLogger('all', {
|
||||||
|
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||||
|
}),
|
||||||
|
extra: {
|
||||||
|
charset: 'utf8mb4_unicode_ci',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connection
|
||||||
90
federation/tsconfig.json
Normal file
90
federation/tsconfig.json
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||||
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||||
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./build", /* Redirect output structure to the directory. */
|
||||||
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
|
||||||
|
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
// "@arg/*": ["src/graphql/arg/*"],
|
||||||
|
// "@enum/*": ["src/graphql/enum/*"],
|
||||||
|
// "@model/*": ["src/graphql/model/*"],
|
||||||
|
"@repository/*": ["src/typeorm/repository/*"],
|
||||||
|
// "@test/*": ["test/*"],
|
||||||
|
/* external */
|
||||||
|
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
|
||||||
|
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
|
||||||
|
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
|
||||||
|
},
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
"typeRoots": ["src/dht_node/@types", "node_modules/@types"], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
|
},
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../database/tsconfig.json",
|
||||||
|
// add 'prepend' if you want to include the referenced project in your output file
|
||||||
|
// "prepend": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3247
federation/yarn.lock
Normal file
3247
federation/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user