Use example project to setup integration tests among the modules.refactor/new-arch
@@ -41,5 +41,10 @@ | |||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||
"publishConfig": { | |||
"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, | |||
it, | |||
expect, | |||
vi, Mock, | |||
Mock, | |||
} from 'vitest'; | |||
import { | |||
app, | |||
Endpoint, | |||
Operation, | |||
composeRecipes, | |||
statusCodes | |||
} 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 {server} from '@modal-sh/yasumi-extender-http/backend'; | |||
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', () => { | |||
let theClient: Client; | |||
let theServer: Server; | |||
let theRawEndpoint: Endpoint; | |||
let theOperation: Operation; | |||
let dataSource: Record<keyof DataSource, Mock>; | |||
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(() => { | |||
@@ -48,27 +41,9 @@ describe('default', () => { | |||
beforeAll(async () => { | |||
const { | |||
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); | |||
@@ -85,6 +60,8 @@ describe('default', () => { | |||
}); | |||
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 | |||
// | |||
// 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 | |||
// object so as the client is not limited to .text(), .json(), .arrayBuffer() etc) | |||
const responseRaw = await theClient | |||
.at(theRawEndpoint) | |||
.at(theEndpoint) | |||
.makeRequest( | |||
theOperation | |||
.search({ | |||
@@ -102,11 +79,13 @@ describe('default', () => { | |||
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'); | |||
}); | |||
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 | |||
// | |||
// 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 | |||
// object so as the client is not limited to .text(), .json(), .arrayBuffer() etc) | |||
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)? | |||
.makeRequest( | |||
theOperation | |||
@@ -125,7 +104,7 @@ describe('default', () => { | |||
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'); | |||
}); | |||
}); |
@@ -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": { | |||
"@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": { | |||
"@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) | |||
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: | |||
'@types/node': | |||
specifier: ^20.11.0 | |||