Use example project to setup integration tests among the modules.refactor/new-arch
@@ -41,5 +41,10 @@ | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | "author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | ||||
"publishConfig": { | "publishConfig": { | ||||
"access": "restricted" | "access": "restricted" | ||||
}, | |||||
"dependencies": { | |||||
"@modal-sh/yasumi": "workspace:*", | |||||
"@modal-sh/yasumi-recipe-resource": "workspace:*", | |||||
"@modal-sh/yasumi-extender-http": "workspace:*" | |||||
} | } | ||||
} | } |
@@ -0,0 +1,34 @@ | |||||
import { | |||||
app, | |||||
composeRecipes, | |||||
} from '@modal-sh/yasumi'; | |||||
import { | |||||
DataSource | |||||
} from '@modal-sh/yasumi/backend'; | |||||
import {server} from '@modal-sh/yasumi-extender-http/backend'; | |||||
import {addResourceRecipe} from '@modal-sh/yasumi-recipe-resource'; | |||||
export const setupApp = (dataSource: DataSource) => { | |||||
const { | |||||
app: theApp, | |||||
operations, | |||||
backend: theBackend, | |||||
} = composeRecipes([ | |||||
addResourceRecipe({ endpointName: 'users', dataSource, }), | |||||
addResourceRecipe({ endpointName: 'posts', dataSource, }) | |||||
])({ | |||||
app: app({ | |||||
name: 'default' as const, | |||||
}), | |||||
}); | |||||
const theServer = typeof theBackend !== 'undefined' ? server({ | |||||
backend: theBackend, | |||||
}) : undefined; | |||||
return { | |||||
app: theApp, | |||||
operations, | |||||
server: theServer, | |||||
}; | |||||
}; |
@@ -4,41 +4,34 @@ import { | |||||
afterAll, | afterAll, | ||||
it, | it, | ||||
expect, | expect, | ||||
vi, Mock, | |||||
Mock, | |||||
} from 'vitest'; | } from 'vitest'; | ||||
import { | import { | ||||
app, | |||||
Endpoint, | |||||
Operation, | |||||
composeRecipes, | |||||
statusCodes | |||||
} from '@modal-sh/yasumi'; | } from '@modal-sh/yasumi'; | ||||
import {DataSource, DataSourceQuery, EmplaceDetails, Server} from '@modal-sh/yasumi/backend'; | |||||
import { | |||||
DataSource, | |||||
Server, | |||||
} from '@modal-sh/yasumi/backend'; | |||||
import {Client} from '@modal-sh/yasumi/client'; | import {Client} from '@modal-sh/yasumi/client'; | ||||
import {server} from '@modal-sh/yasumi-extender-http/backend'; | |||||
import {client} from '@modal-sh/yasumi-extender-http/client'; | import {client} from '@modal-sh/yasumi-extender-http/client'; | ||||
import {addResourceRecipe, ResourceItemFetchedResponse} from '@modal-sh/yasumi-recipe-resource'; | |||||
import {ResourceItemFetchedResponse} from '@modal-sh/yasumi-recipe-resource'; | |||||
import {createDummyDataSource} from './fixtures/data-source'; | |||||
import {setupApp} from '../src/setup'; | |||||
const connectionParams = { | |||||
port: 3001, | |||||
}; | |||||
describe('default', () => { | describe('default', () => { | ||||
let theClient: Client; | let theClient: Client; | ||||
let theServer: Server; | let theServer: Server; | ||||
let theRawEndpoint: Endpoint; | |||||
let theOperation: Operation; | |||||
let dataSource: Record<keyof DataSource, Mock>; | let dataSource: Record<keyof DataSource, Mock>; | ||||
beforeAll(() => { | beforeAll(() => { | ||||
dataSource = { | |||||
create: vi.fn(async (data) => data), | |||||
getById: vi.fn(async () => ({})), | |||||
delete: vi.fn(), | |||||
emplace: vi.fn(async () => [{}, { isCreated: false }]), | |||||
getMultiple: vi.fn(async () => []), | |||||
getSingle: vi.fn(async () => ({})), | |||||
getTotalCount: vi.fn(async () => 1), | |||||
newId: vi.fn(async () => 1), | |||||
patch: vi.fn(async (id, data) => ({ ...data, id })), | |||||
initialize: vi.fn(async () => {}), | |||||
}; | |||||
dataSource = createDummyDataSource(); | |||||
}); | }); | ||||
afterAll(() => { | afterAll(() => { | ||||
@@ -48,27 +41,9 @@ describe('default', () => { | |||||
beforeAll(async () => { | beforeAll(async () => { | ||||
const { | const { | ||||
app: theApp, | app: theApp, | ||||
operations, | |||||
backend: theBackend, | |||||
} = composeRecipes([ | |||||
addResourceRecipe({ endpointName: 'users', dataSource, }), | |||||
addResourceRecipe({ endpointName: 'posts', dataSource, }) | |||||
])({ | |||||
app: app({ | |||||
name: 'default' as const, | |||||
}), | |||||
}); | |||||
theRawEndpoint = theApp.endpoints.get('users'); | |||||
theOperation = operations.fetch; | |||||
theServer = server({ | |||||
backend: theBackend, | |||||
}); | |||||
const connectionParams = { | |||||
port: 3001, | |||||
}; | |||||
server: createdServer, | |||||
} = setupApp(dataSource); | |||||
theServer = createdServer; | |||||
await theServer.serve(connectionParams); | await theServer.serve(connectionParams); | ||||
@@ -85,6 +60,8 @@ describe('default', () => { | |||||
}); | }); | ||||
it('works', async () => { | it('works', async () => { | ||||
const theEndpoint = theClient.app.endpoints.get('users'); | |||||
const theOperation = theClient.app.operations.get('fetch'); | |||||
// TODO create wrapper for fetch's Response here | // TODO create wrapper for fetch's Response here | ||||
// | // | ||||
// should we create a helper object to process client-side received response from server's sent response? | // should we create a helper object to process client-side received response from server's sent response? | ||||
@@ -92,7 +69,7 @@ describe('default', () => { | |||||
// the motivation is to remove the manual deserialization from the client (provide serialization on the response | // the motivation is to remove the manual deserialization from the client (provide serialization on the response | ||||
// object so as the client is not limited to .text(), .json(), .arrayBuffer() etc) | // object so as the client is not limited to .text(), .json(), .arrayBuffer() etc) | ||||
const responseRaw = await theClient | const responseRaw = await theClient | ||||
.at(theRawEndpoint) | |||||
.at(theEndpoint) | |||||
.makeRequest( | .makeRequest( | ||||
theOperation | theOperation | ||||
.search({ | .search({ | ||||
@@ -102,11 +79,13 @@ describe('default', () => { | |||||
const response = ResourceItemFetchedResponse.fromFetchResponse(responseRaw); | const response = ResourceItemFetchedResponse.fromFetchResponse(responseRaw); | ||||
expect(response).toHaveProperty('statusCode', 200); | |||||
expect(response).toHaveProperty('statusCode', statusCodes.HTTP_STATUS_OK); | |||||
expect(response).toHaveProperty('statusMessage', 'Resource Collection Fetched'); | expect(response).toHaveProperty('statusMessage', 'Resource Collection Fetched'); | ||||
}); | }); | ||||
it('works for items', async () => { | it('works for items', async () => { | ||||
const theEndpoint = theClient.app.endpoints.get('users'); | |||||
const theOperation = theClient.app.operations.get('fetch'); | |||||
// TODO create wrapper for fetch's Response here | // TODO create wrapper for fetch's Response here | ||||
// | // | ||||
// should we create a helper object to process client-side received response from server's sent response? | // should we create a helper object to process client-side received response from server's sent response? | ||||
@@ -114,7 +93,7 @@ describe('default', () => { | |||||
// the motivation is to remove the manual deserialization from the client (provide serialization on the response | // the motivation is to remove the manual deserialization from the client (provide serialization on the response | ||||
// object so as the client is not limited to .text(), .json(), .arrayBuffer() etc) | // object so as the client is not limited to .text(), .json(), .arrayBuffer() etc) | ||||
const responseRaw = await theClient | const responseRaw = await theClient | ||||
.at(theRawEndpoint, { resourceId: 3 }) | |||||
.at(theEndpoint, { resourceId: 3 }) | |||||
// TODO how to inject extra data (e.g. headers, body) in the operation (e.g. auth)? | // TODO how to inject extra data (e.g. headers, body) in the operation (e.g. auth)? | ||||
.makeRequest( | .makeRequest( | ||||
theOperation | theOperation | ||||
@@ -125,7 +104,7 @@ describe('default', () => { | |||||
const response = ResourceItemFetchedResponse.fromFetchResponse(responseRaw); | const response = ResourceItemFetchedResponse.fromFetchResponse(responseRaw); | ||||
expect(response).toHaveProperty('statusCode', 200); | |||||
expect(response).toHaveProperty('statusCode', statusCodes.HTTP_STATUS_OK); | |||||
expect(response).toHaveProperty('statusMessage', 'Resource Item Fetched'); | expect(response).toHaveProperty('statusMessage', 'Resource Item Fetched'); | ||||
}); | }); | ||||
}); | }); |
@@ -0,0 +1,14 @@ | |||||
import {vi} from 'vitest'; | |||||
export const createDummyDataSource = () => ({ | |||||
create: vi.fn(async (data) => data), | |||||
getById: vi.fn(async () => ({})), | |||||
delete: vi.fn(), | |||||
emplace: vi.fn(async () => [{}, { isCreated: false }]), | |||||
getMultiple: vi.fn(async () => []), | |||||
getSingle: vi.fn(async () => ({})), | |||||
getTotalCount: vi.fn(async () => 1), | |||||
newId: vi.fn(async () => 1), | |||||
patch: vi.fn(async (id, data) => ({ ...data, id })), | |||||
initialize: vi.fn(async () => {}), | |||||
}); |
@@ -1,8 +0,0 @@ | |||||
import { describe, it, expect } from 'vitest'; | |||||
import add from '../src'; | |||||
describe('blah', () => { | |||||
it('works', () => { | |||||
expect(add(1, 1)).toEqual(2); | |||||
}); | |||||
}); |
@@ -45,5 +45,35 @@ | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@modal-sh/yasumi": "workspace:*" | "@modal-sh/yasumi": "workspace:*" | ||||
}, | |||||
"exports": { | |||||
"./backend": { | |||||
"development": { | |||||
"require": "./dist/cjs/development/backend.js", | |||||
"import": "./dist/esm/development/backend.js" | |||||
}, | |||||
"require": "./dist/cjs/production/backend.js", | |||||
"import": "./dist/esm/production/backend.js", | |||||
"types": "./dist/types/backend/index.d.ts" | |||||
}, | |||||
"./client": { | |||||
"development": { | |||||
"require": "./dist/cjs/development/client.js", | |||||
"import": "./dist/esm/development/client.js" | |||||
}, | |||||
"require": "./dist/cjs/production/client.js", | |||||
"import": "./dist/esm/production/client.js", | |||||
"types": "./dist/types/client/index.d.ts" | |||||
} | |||||
}, | |||||
"typesVersions": { | |||||
"*": { | |||||
"backend": [ | |||||
"./dist/types/backend/index.d.ts" | |||||
], | |||||
"client": [ | |||||
"./dist/types/client/index.d.ts" | |||||
] | |||||
} | |||||
} | } | ||||
} | } |
@@ -1,191 +0,0 @@ | |||||
import {describe, it, expect, beforeAll, afterAll} from 'vitest'; | |||||
import { | |||||
App, | |||||
app, | |||||
Endpoint, | |||||
endpoint, | |||||
Operation, | |||||
operation, | |||||
statusCodes, | |||||
validation as v, | |||||
} from '../../../core/src/common'; | |||||
import {Backend, backend, DataSource, Server} from '../../../core/src/backend'; | |||||
import {Client} from '../../../core/src/client'; | |||||
import {server} from '../../../core/src/extenders/http/backend'; | |||||
import {client} from '../../../core/src/extenders/http/client'; | |||||
const op = operation({ | |||||
name: 'create' as const, | |||||
method: 'POST' as const, | |||||
}); | |||||
const e = endpoint({ | |||||
name: 'e' as const, | |||||
schema: v.object({}), | |||||
}) | |||||
.can('create'); | |||||
const a = app({ | |||||
name: 'foo' as const, | |||||
}) | |||||
.operation(op) | |||||
.endpoint(e); | |||||
describe('app', () => { | |||||
let theApp: App; | |||||
let theBackend: Backend; | |||||
let theClient: Client; | |||||
let theDataSource: DataSource; | |||||
let theEndpoint: Endpoint; | |||||
let theServer: Server; | |||||
let theOperation: Operation; | |||||
beforeAll(async () => { | |||||
theOperation = operation({ | |||||
name: 'fetch' as const, | |||||
}); | |||||
theEndpoint = endpoint({ | |||||
name: 'users' as const, | |||||
schema: v.object({ | |||||
username: v.string() | |||||
}), | |||||
}) | |||||
.param('resourceId') | |||||
.can('fetch'); | |||||
theApp = app({ | |||||
name: 'foo' as const | |||||
}) | |||||
.operation(theOperation) | |||||
.endpoint(theEndpoint); | |||||
theBackend = backend({ | |||||
app: theApp | |||||
}); | |||||
// add recipes function that will wrap app and backend to add operations and implement them, and will return a set | |||||
// of operations. | |||||
// | |||||
// recipes should have a backend and client counterpart. | |||||
theBackend.implementOperation('fetch', async (ctx) => { | |||||
// noop | |||||
}); | |||||
theServer = server({ | |||||
backend: theBackend | |||||
}); | |||||
const connectionParams = { | |||||
port: 3000, | |||||
}; | |||||
await theServer.serve(connectionParams); | |||||
theClient = client({ | |||||
app: theApp | |||||
}); | |||||
await theClient.connect(connectionParams); | |||||
}); | |||||
afterAll(async () => { | |||||
await theClient.disconnect(); | |||||
await theServer.close(); | |||||
}); | |||||
it('works', async () => { | |||||
const response = await theClient | |||||
.at(theEndpoint, { resourceId: 3 }) | |||||
.makeRequest(theOperation); | |||||
expect(response).toHaveProperty('status', statusCodes.HTTP_STATUS_UNPROCESSABLE_ENTITY); | |||||
}); | |||||
}); | |||||
// const theEndpoint = endpoint({ | |||||
// schema: v.object({ | |||||
// username: v.string(), | |||||
// }), | |||||
// }) | |||||
// .can('patch') | |||||
// .can('query'); | |||||
// | |||||
// const canPatch = operation({ | |||||
// name: 'patch' as const, | |||||
// args: [ | |||||
// 'merge', | |||||
// 'delta', | |||||
// ] as const, | |||||
// // TODO define resource-specific stuff, like defining URL params, etc. | |||||
// }); | |||||
// | |||||
// const canFetch = operation({ | |||||
// name: 'fetch' as const, | |||||
// args: [ | |||||
// 'item', | |||||
// 'default', | |||||
// ] as const, | |||||
// }); | |||||
// | |||||
// const canQuery = operation({ | |||||
// name: 'query' as const, | |||||
// }); | |||||
// | |||||
// const canCreate = operation({ | |||||
// name: 'create' as const, | |||||
// }); | |||||
// | |||||
// const canEmplace = operation({ | |||||
// name: 'emplace' as const, | |||||
// }); | |||||
// | |||||
// const canDelete = operation({ | |||||
// name: 'delete' as const, | |||||
// }); | |||||
// | |||||
// export const theApp = app({ | |||||
// name: 'foo' as const, | |||||
// }) | |||||
// .operation(canQuery) | |||||
// .operation(canPatch) | |||||
// .operation(canFetch) | |||||
// .operation(canCreate) | |||||
// .operation(canEmplace) | |||||
// .operation(canDelete) | |||||
// .endpoint(theEndpoint); | |||||
// // | |||||
// // const bootstrap = async (theApp: App) => { | |||||
// // if (typeof window === 'undefined') { | |||||
// // const { backend } = await import('./backend'); | |||||
// // const theBackend = backend({ | |||||
// // app: theApp | |||||
// // }); | |||||
// // } | |||||
// // }; | |||||
// | |||||
// const b = backend({ | |||||
// app: theApp, | |||||
// }) | |||||
// .implementOperation({ | |||||
// operation: 'fetch' as const, | |||||
// implementation: ({ | |||||
// endpoint, | |||||
// arg | |||||
// }) => { | |||||
// switch (arg) { | |||||
// case 'default': { | |||||
// | |||||
// } | |||||
// } | |||||
// }, | |||||
// }); | |||||
// | |||||
// const s = server({ | |||||
// backend: b, | |||||
// }) | |||||
// .serve({ | |||||
// host: '0.0.0.0', | |||||
// port: 3000, | |||||
// basePath: '/api' | |||||
// }); |
@@ -45,5 +45,22 @@ | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@modal-sh/yasumi": "workspace:*" | "@modal-sh/yasumi": "workspace:*" | ||||
}, | |||||
"types": "./dist/types/index.d.ts", | |||||
"main": "./dist/cjs/production/index.js", | |||||
"module": "./dist/esm/production/index.js", | |||||
"exports": { | |||||
".": { | |||||
"development": { | |||||
"require": "./dist/cjs/development/index.js", | |||||
"import": "./dist/esm/development/index.js" | |||||
}, | |||||
"require": "./dist/cjs/production/index.js", | |||||
"import": "./dist/esm/production/index.js", | |||||
"types": "./dist/types/index.d.ts" | |||||
} | |||||
}, | |||||
"typesVersions": { | |||||
"*": {} | |||||
} | } | ||||
} | } |
@@ -76,6 +76,16 @@ importers: | |||||
version: 1.2.0(@types/node@20.11.0) | version: 1.2.0(@types/node@20.11.0) | ||||
packages/examples/http-resource-server: | packages/examples/http-resource-server: | ||||
dependencies: | |||||
'@modal-sh/yasumi': | |||||
specifier: workspace:* | |||||
version: link:../../core | |||||
'@modal-sh/yasumi-extender-http': | |||||
specifier: workspace:* | |||||
version: link:../../extenders/http | |||||
'@modal-sh/yasumi-recipe-resource': | |||||
specifier: workspace:* | |||||
version: link:../../recipes/resource | |||||
devDependencies: | devDependencies: | ||||
'@types/node': | '@types/node': | ||||
specifier: ^20.11.0 | specifier: ^20.11.0 | ||||