diff --git a/packages/event-logger/agent.yml b/packages/event-logger/agent.yml index c835894be..cfbabc005 100644 --- a/packages/event-logger/agent.yml +++ b/packages/event-logger/agent.yml @@ -4,8 +4,9 @@ constants: baseUrl: http://localhost:3335 port: 3335 methods: - - loggerGetAuditEvents - - loggerLogAuditEvent + - persistState + - loadState + - deleteExpiredStates dbConnection: $require: typeorm?t=function#createConnection @@ -17,7 +18,7 @@ dbConnection: migrations: $require: './packages/data-store?t=object#DataStoreMigrations' entities: - $require: './packages/data-store?t=object#DataStoreEventLoggerEntities' + $require: './packages/data-store?t=object#DataStoreXStateStoreEntities' server: baseUrl: @@ -81,9 +82,9 @@ agent: $args: - schemaValidation: false plugins: - - $require: ./packages/event-logger/dist#EventLogger + - $require: ./packages/xstate-persistence/dist#XStatePersistence $args: - store: - $require: './packages/data-store/dist#EventLoggerStore' + $require: './packages/data-store/dist#XStateStore' $args: - $ref: /dbConnection diff --git a/packages/xstate-persistence/package.json b/packages/xstate-persistence/package.json index fc2552651..acb740b90 100644 --- a/packages/xstate-persistence/package.json +++ b/packages/xstate-persistence/package.json @@ -14,20 +14,18 @@ "build:clean": "tsc --build --clean && tsc --build", "generate-plugin-schema": "ts-node ../../packages/dev/bin/sphereon.js dev generate-plugin-schema" }, - "dependencies": { + + "devDependencies": { "@sphereon/ssi-sdk.agent-config": "workspace:*", "@sphereon/ssi-sdk.core": "workspace:*", "@sphereon/ssi-sdk.data-store": "workspace:*", - "@sphereon/ssi-sdk.vc-status-list": "workspace:*", - "@sphereon/ssi-sdk.vc-status-list-issuer-drivers": "workspace:*", - "@sphereon/ssi-types": "workspace:*", - "@veramo/core": "4.2.0" - }, - "devDependencies": { "@types/node": "^20.11.17", "@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/parser": "^4.31.1", + "@veramo/remote-client": "4.2.0", + "@veramo/remote-server": "4.2.0", "ts-node": "^10.9.1", + "typeorm": "0.3.17", "typescript": "4.9.5" }, "files": [ diff --git a/packages/xstate-persistence/plugin.schema.json b/packages/xstate-persistence/plugin.schema.json index 722ce770b..f97d81b5e 100644 --- a/packages/xstate-persistence/plugin.schema.json +++ b/packages/xstate-persistence/plugin.schema.json @@ -80,25 +80,6 @@ "updatedAt" ] }, - "XStatePersistenceEvent": { - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/XStatePersistenceEventType" - }, - "data": { - "$ref": "#/components/schemas/NonPersistedXStatePersistenceEvent" - } - }, - "required": [ - "type", - "data" - ] - }, - "XStatePersistenceEventType": { - "type": "string", - "const": "every" - }, "NonPersistedXStatePersistenceEvent": { "$ref": "#/components/schemas/SaveStateArgs" }, @@ -131,9 +112,6 @@ "state", "type" ] - }, - "OnEventResult": { - "$ref": "#/components/schemas/VoidResult" } }, "methods": { @@ -155,13 +133,13 @@ "$ref": "#/components/schemas/LoadStateResult" } }, - "onEvent": { + "persistState": { "description": "Persists the state whenever an event is emitted", "arguments": { - "$ref": "#/components/schemas/XStatePersistenceEvent" + "$ref": "#/components/schemas/NonPersistedXStatePersistenceEvent" }, "returnType": { - "$ref": "#/components/schemas/OnEventResult" + "$ref": "#/components/schemas/State" } } } diff --git a/packages/xstate-persistence/src/__tests__/localAgent.test.ts b/packages/xstate-persistence/src/__tests__/localAgent.test.ts new file mode 100644 index 000000000..fda85a4ec --- /dev/null +++ b/packages/xstate-persistence/src/__tests__/localAgent.test.ts @@ -0,0 +1,34 @@ +import { DataSource } from 'typeorm' +import { createObjects, getConfig } from '@sphereon/ssi-sdk.agent-config' + +jest.setTimeout(60000) + +import xStatePersistenceAgentLogic from './shared/xStatePersistenceAgentLogic' + +let dbConnection: Promise +let agent: any + +const setup = async (): Promise => { + const config = await getConfig('packages/event-logger/agent.yml') + const { localAgent, db } = await createObjects(config, { localAgent: '/agent', db: '/dbConnection' }) + agent = localAgent + dbConnection = db + + return true +} + +const tearDown = async (): Promise => { + await (await dbConnection).destroy() + return true +} + +const getAgent = () => agent +const testContext = { + getAgent, + setup, + tearDown, +} + +describe('Local integration tests', (): void => { + xStatePersistenceAgentLogic(testContext) +}) diff --git a/packages/xstate-persistence/src/__tests__/restAgent.test.ts b/packages/xstate-persistence/src/__tests__/restAgent.test.ts new file mode 100644 index 000000000..0b7066e28 --- /dev/null +++ b/packages/xstate-persistence/src/__tests__/restAgent.test.ts @@ -0,0 +1,71 @@ +import 'cross-fetch/polyfill' +import {createAgent, IAgent, IAgentOptions} from '@veramo/core' +import {AgentRestClient} from '@veramo/remote-client' +import {AgentRouter, RequestWithAgentRouter} from '@veramo/remote-server' +// @ts-ignore +import express, {Router} from 'express' +import {Server} from 'http' +import {DataSource} from 'typeorm' +import {createObjects, getConfig} from '@sphereon/ssi-sdk.agent-config' +import {IXStatePersistence} from "../index"; +import xStatePersistenceAgentLogic from './shared/xStatePersistenceAgentLogic' + +jest.setTimeout(60000) + +const port = 3002 +const basePath = '/agent' + +let serverAgent: IAgent +let restServer: Server +let dbConnection: Promise + +const getAgent = (options?: IAgentOptions) => + createAgent({ + ...options, + plugins: [ + new AgentRestClient({ + url: 'http://localhost:' + port + basePath, + enabledMethods: serverAgent.availableMethods(), + schema: serverAgent.getSchema(), + }), + ], + }) + +const setup = async (): Promise => { + const config = await getConfig('packages/event-logger/agent.yml') + const { agent, db } = await createObjects(config, { agent: '/agent', db: '/dbConnection' }) + serverAgent = agent + dbConnection = db + + const agentRouter: Router = AgentRouter({ + exposedMethods: serverAgent.availableMethods(), + }) + + const requestWithAgent: Router = RequestWithAgentRouter({ + agent: serverAgent, + }) + + return new Promise((resolve): void => { + const app = express() + app.use(basePath, requestWithAgent, agentRouter) + restServer = app.listen(port, () => { + resolve(true) + }) + }) +} + +const tearDown = async (): Promise => { + restServer.close() + await (await dbConnection).destroy() + return true +} + +const testContext = { + getAgent, + setup, + tearDown, +} + +describe('REST integration tests', (): void => { + xStatePersistenceAgentLogic(testContext) +}) diff --git a/packages/xstate-persistence/src/__tests__/shared/xStatePersistenceAgentLogic.ts b/packages/xstate-persistence/src/__tests__/shared/xStatePersistenceAgentLogic.ts new file mode 100644 index 000000000..49ae0d990 --- /dev/null +++ b/packages/xstate-persistence/src/__tests__/shared/xStatePersistenceAgentLogic.ts @@ -0,0 +1,85 @@ +import {NonPersistedXStateStoreEvent, State} from "@sphereon/ssi-sdk.data-store"; +import {TAgent} from '@veramo/core' +import {IXStatePersistence, SQLDialect} from '../../index' + +type ConfiguredAgent = TAgent + +export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Promise; tearDown: () => Promise }): void => { + describe('Event Logger Agent Plugin', (): void => { + let agent: ConfiguredAgent + + beforeAll(async (): Promise => { + await testContext.setup() + agent = testContext.getAgent() + }) + + afterAll(testContext.tearDown) + + it('should store xstate event', async (): Promise => { + const xstateEvent: NonPersistedXStateStoreEvent = { + state: 'test_state', + type: 'b40b8474-58a2-4b23-9fde-bd6ee1902cdb', + createdAt: new Date(), + updatedAt: new Date(), + completedAt: new Date(), + tenantId: 'test_tenant_id' + } + + const savedXStoreEvent: State = await agent.persistState(xstateEvent) + expect(savedXStoreEvent).toBeDefined() + }) + + it('should retrieve an xstate event', async (): Promise => { + const xstateEvent: NonPersistedXStateStoreEvent = { + state: 'test_state', + type: 'b40b8474-58a2-4b23-9fde-bd6ee1902cdb', + createdAt: new Date(), + updatedAt: new Date(), + completedAt: new Date(), + tenantId: 'test_tenant_id' + } + + const savedXStoreEvent: State = await agent.persistState({...xstateEvent}) + expect(savedXStoreEvent).toBeDefined() + + const result: State = await agent.loadState({ type: savedXStoreEvent.type }) + expect(result).toBeDefined() + }) + + it('should delete the expired records', async () => { + const now = new Date() + const newestXstateEvent: NonPersistedXStateStoreEvent = { + state: 'test_state', + type: 'test_type_1', + createdAt: now, + completedAt: new Date(), + tenantId: 'test_tenant_id' + } + const middleXstateEvent: NonPersistedXStateStoreEvent = { + state: 'test_state', + type: 'test_type_2', + createdAt: new Date(+now - 30000), + completedAt: new Date(), + tenantId: 'test_tenant_id' + } + + const oldestXstateEvent: NonPersistedXStateStoreEvent = { + state: 'test_state', + type: 'test_type_3', + createdAt: new Date(+now - 60000), + completedAt: new Date(), + tenantId: 'test_tenant_id' + } + + await agent.persistState(newestXstateEvent) + await agent.persistState(middleXstateEvent) + await agent.persistState(oldestXstateEvent) + + await agent.deleteExpiredStates({ duration: 40000, dialect: SQLDialect.SQLite3 }) + + await expect(agent.loadState({ type: 'test_type_1'})).resolves.toBeDefined() + await expect(agent.loadState({ type: 'test_type_2'})).resolves.toBeDefined() + await expect(agent.loadState({ type: 'test_type_3'})).rejects.toEqual(Error('No state found for type: test_type_3')) + }) + }) +} diff --git a/packages/xstate-persistence/src/agent/XStatePersistence.ts b/packages/xstate-persistence/src/agent/XStatePersistence.ts index 78020b881..89b6286fa 100644 --- a/packages/xstate-persistence/src/agent/XStatePersistence.ts +++ b/packages/xstate-persistence/src/agent/XStatePersistence.ts @@ -1,12 +1,13 @@ -import {IAbstractXStateStore} from "@sphereon/ssi-sdk.data-store"; +import {DeleteStateArgs, IAbstractXStateStore, State} from "@sphereon/ssi-sdk.data-store"; import {IAgentPlugin,} from '@veramo/core' import { DeleteExpiredStatesArgs, DeleteStateResult, - OnEventResult, + NonPersistedXStatePersistenceEvent, RequiredContext, schema, + SQLDialect, XStatePersistenceEvent, XStatePersistenceEventType, XStateStateManagerOptions @@ -35,21 +36,28 @@ export class XStatePersistence implements IAgentPlugin { this.methods = { loadState: this.loadState.bind(this), deleteExpiredStates: this.deleteExpiredStates.bind(this), - onEvent: this.onEvent.bind(this) + persistState: this.persistState.bind(this) } } - async onEvent(event: XStatePersistenceEvent, context: RequiredContext): Promise { + public async onEvent(event: XStatePersistenceEvent, context: RequiredContext): Promise { switch (event.type) { case XStatePersistenceEventType.EVERY: // Calling the context of the agent to make sure the REST client is called when configured - await context.agent.persistState(event.data) + await context.agent.persistState({...event.data}) break default: return Promise.reject(Error('Event type not supported')) } } + private async persistState(args: NonPersistedXStatePersistenceEvent): Promise { + if (!this.store) { + return Promise.reject(Error('No store available in options')) + } + return this.store.saveState(args) + } + private async loadState(args: LoadStateArgs): Promise { if (!this.store) { return Promise.reject(Error('No store available in options')) @@ -61,22 +69,22 @@ export class XStatePersistence implements IAgentPlugin { if (!this.store) { return Promise.reject(Error('No store available in options')) } + const sqLiteParams: DeleteStateArgs = { + where: `created_at < datetime('now', :duration)`, + parameters: { + duration: `-${args.duration / 1000} seconds` + } + } + const postgreSQLParams: DeleteStateArgs = { + where: 'created_at < :duration', + parameters: { + duration: `NOW() - ${args.duration / 1000} seconds::interval` + } + } switch (args.dialect) { - case 'SQLite3': - const sqLiteParams = { - where: `created_at < datetime('now', :duration)`, - params: { - duration: `-${args.duration / 1000} seconds` - } - } + case SQLDialect.SQLite3: return this.store.deleteState(sqLiteParams) - case 'PostgreSQL': - const postgreSQLParams = { - where: 'created_at < :duration', - params: { - duration: `NOW() - '${args.duration / 1000} seconds'::interval` - } - } + case SQLDialect.PostgreSQL: return this.store.deleteState(postgreSQLParams) default: return Promise.reject(Error('Invalid database dialect')) diff --git a/packages/xstate-persistence/src/types/IXStatePersistence.ts b/packages/xstate-persistence/src/types/IXStatePersistence.ts index b859ae098..5f86c2d03 100644 --- a/packages/xstate-persistence/src/types/IXStatePersistence.ts +++ b/packages/xstate-persistence/src/types/IXStatePersistence.ts @@ -1,3 +1,4 @@ +import {State} from "@sphereon/ssi-sdk.data-store"; import {IPluginMethodMap} from '@veramo/core' import { @@ -5,9 +6,7 @@ import { DeleteStateResult, LoadStateArgs, LoadStateResult, - OnEventResult, - RequiredContext, - XStatePersistenceEvent + NonPersistedXStatePersistenceEvent, RequiredContext } from "./types"; /** @@ -45,12 +44,12 @@ export interface IXStatePersistence extends IPluginMethodMap { /** * Persists the state whenever an event is emitted - * @param event XStatePersistenceEvent + * @param event NonPersistedXStatePersistenceEvent * type of the event ('every' is the only one available at the moment) * data of the event * * @param context * @beta This API is likely to change without a BREAKING CHANGE notice */ - onEvent(event: XStatePersistenceEvent, context: RequiredContext): Promise + persistState(event: NonPersistedXStatePersistenceEvent, context: RequiredContext): Promise } diff --git a/packages/xstate-persistence/src/types/types.ts b/packages/xstate-persistence/src/types/types.ts index 53d584152..ec2b4049b 100644 --- a/packages/xstate-persistence/src/types/types.ts +++ b/packages/xstate-persistence/src/types/types.ts @@ -27,8 +27,6 @@ export type LoadStateResult = State export type DeleteStateResult = VoidResult -export type OnEventResult = VoidResult - export type XStatePersistenceEvent = { type: XStatePersistenceEventType, data: NonPersistedXStatePersistenceEvent diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fac6a0e21..28418481f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2631,6 +2631,10 @@ importers: packages/xstate-persistence: dependencies: + react-native-securerandom: + specifier: ^1.0.1 + version: 1.0.1(react-native@0.73.2) + devDependencies: '@sphereon/ssi-sdk.agent-config': specifier: workspace:* version: link:../agent-config @@ -2640,22 +2644,6 @@ importers: '@sphereon/ssi-sdk.data-store': specifier: workspace:* version: link:../data-store - '@sphereon/ssi-sdk.vc-status-list': - specifier: workspace:* - version: link:../vc-status-list - '@sphereon/ssi-sdk.vc-status-list-issuer-drivers': - specifier: workspace:* - version: link:../vc-status-list-issuer-drivers - '@sphereon/ssi-types': - specifier: workspace:* - version: link:../ssi-types - '@veramo/core': - specifier: 4.2.0 - version: 4.2.0(patch_hash=c5oempznsz4br5w3tcuk2i2mau) - react-native-securerandom: - specifier: ^1.0.1 - version: 1.0.1(react-native@0.73.2) - devDependencies: '@types/node': specifier: ^20.11.17 version: 20.11.19 @@ -2665,9 +2653,18 @@ importers: '@typescript-eslint/parser': specifier: ^4.31.1 version: 4.33.0(eslint@7.32.0)(typescript@4.9.5) + '@veramo/remote-client': + specifier: 4.2.0 + version: 4.2.0 + '@veramo/remote-server': + specifier: 4.2.0 + version: 4.2.0(express@4.18.2) ts-node: specifier: ^10.9.1 version: 10.9.2(@types/node@20.11.19)(typescript@4.9.5) + typeorm: + specifier: 0.3.17 + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.7)(ts-node@10.9.2) typescript: specifier: 4.9.5 version: 4.9.5 @@ -5771,7 +5768,7 @@ packages: detect-libc: 2.0.2 https-proxy-agent: 5.0.1 make-dir: 3.1.0 - node-fetch: 2.6.12 + node-fetch: 2.7.0 nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 @@ -6475,7 +6472,7 @@ packages: '@octokit/request-error': 3.0.3 '@octokit/types': 9.3.2 is-plain-object: 5.0.0 - node-fetch: 2.6.7 + node-fetch: 2.7.0 universal-user-agent: 6.0.1 transitivePeerDependencies: - encoding @@ -17474,19 +17471,6 @@ packages: dependencies: http2-client: 1.3.5 - /node-fetch@2.6.12: - resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} - engines: {node: 4.x || >=6.0.0} - requiresBuild: true - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - optional: true - /node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0}