Skip to content
This repository has been archived by the owner on Jun 14, 2021. It is now read-only.

Commit

Permalink
feat: add context (#56)
Browse files Browse the repository at this point in the history
* feat: use context argument
  • Loading branch information
aimed authored Jun 23, 2019
1 parent b2eb8b3 commit 8aad61e
Show file tree
Hide file tree
Showing 20 changed files with 120 additions and 89 deletions.
110 changes: 60 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@
🚧 Early prototype ahead 🚧

# Slushy - fully typed and validated APIs 🍦🍭

Slushy uses OAS (OpenApi Specification) schemas to generate server boilerplate and validate server inputs and outputs.
Slushy currently consist of the following parts:
- [@slushy/codegen](./codegen) Takes an OAS schema and generates typescript type definitions as well as @slushy/server boilerplate.
- [@slushy/server](./server) An opinionated NodeJS server based on an OAS schema with inputs/outputs validation and out of the box functionality such as api documentation.

- [@slushy/codegen](./codegen) Takes an OAS schema and generates typescript type definitions as well as @slushy/server boilerplate.
- [@slushy/server](./server) An opinionated NodeJS server based on an OAS schema with inputs/outputs validation and out of the box functionality such as api documentation.

To use slushy just follow three simple steps:
- Define the OAS schema
- Generate typescript types and server boilerplate
- Run the server

- Define the OAS schema
- Generate typescript types and server boilerplate
- Run the server

Current features:
- Input validation
- Type generation
- Route generation

- Input validation
- Type generation
- Route generation

## How does it work

**First**: Define your OAS Schema ✒️

It's just like swagger. The following example defines one route `/pets` that will return an Array of Pets.
Expand All @@ -27,86 +32,91 @@ It's just like swagger. The following example defines one route `/pets` that wil
openapi: 3.0.0

paths:
# This will define the resource 'PetsResource'
/pets:
get:
# This is required right now
operationId: getPets
responses:
'200':
description: A lot of pets
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/pet'
# This will define the resource 'PetsResource'
/pets:
get:
# This is required right now
operationId: getPets
responses:
'200':
description: A lot of pets
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/pet'
components:
schemas:
# This will define the type 'Pet'
pet:
type: object
properties:
id:
type: number
name:
type: string
required: ['id', 'name']
additionalProperties: false
schemas:
# This will define the type 'Pet'
pet:
type: object
properties:
id:
type: number
name:
type: string
required: ['id', 'name']
additionalProperties: false
```
**Then**: Generate code using ```yarn gen <schemaFile> <outputDir>``` 🎩
**Then**: Generate code using `yarn gen <schemaFile> <outputDir>` 🎩

This will generate the following:
- A `dir/types.ts` file, that contains all objects (e.g. `Pet`). These are types you can use to implement you code.
- Multiple resource type stubs in `dir/resources`, e.g. `PetResource`. These are controller interface you have to implement.
- A ResourceDefinition that you have to pass to `Slushy`, which will bind the OAS paths to your controllers.

- A `dir/types.ts` file, that contains all objects (e.g. `Pet`). These are types you can use to implement you code.
- Multiple resource type stubs in `dir/resources`, e.g. `PetResource`. These are controller interface you have to implement.
- A ResourceDefinition that you have to pass to `Slushy`, which will bind the OAS paths to your controllers.

<details>
<summary>Show generated code</summary>

Pet (generated):

```ts
export type Pet = {
id: number
name: string
id: number
name: string
}
```

PetsResource (generated):

```ts
export type GetPetsParams = {}
export type GetPetsResponse = Array<Pet>
// You have to implement this
export interface PetsResource<TContext = {}> {
getPets(params: GetPetsParams, context: SlushyContext<TContext>): Promise<GetPetsResponse>
export interface PetsResource<TContext> {
getPets(params: GetPetsParams, context: SlushyContext<TContext>): Promise<GetPetsResponse>
}
```

</details>

**Last**: Setup the server

It's easy:

```ts
async function run() {
const slushy = await Slushy.create({
resourceConfiguration: new ResourceConfig({
PetsResource: new PetsResourceImpl()
})
PetsResource: new PetsResourceImpl(),
}),
})
await slushy.start(3031)
}
run()
```


# TODO
- [ ] Create/use response types
- [ ] Validate output
- [ ] Use input/output formats
- [ ] Handle dates
- [ ] Add security, like helmet etc.
- [ ] Implement authentication

- [ ] Create/use response types
- [ ] Validate output
- [ ] Use input/output formats
- [ ] Handle dates
- [ ] Add security, like helmet etc.
- [ ] Implement authentication
8 changes: 6 additions & 2 deletions codegen/src/generators/ResourcesGenerator/ResourceFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export class ResourceFactory {
*/
create(resourceName: string, operations: ResourceOperation[], tsFile: TSFile): string {
const resourceDescriptionName = `${resourceName}Resource`
const interfaceBuilder = new TSInterfaceBuilder(resourceDescriptionName, 'TContext = {}')
const interfaceBuilder = new TSInterfaceBuilder(resourceDescriptionName, 'TContext')
tsFile.import('SlushyContext', '@slushy/server', true)

for (const operation of operations) {
tsFile.import(operation.returnType)
Expand All @@ -27,7 +28,10 @@ export class ResourceFactory {
interfaceBuilder.addMethod({
name: operation.name,
returnType: `Promise<${operation.returnType}>`,
parameters: [{ name: 'params', type: operation.parameterType }, { name: 'context', type: 'TContext' }],
parameters: [
{ name: 'params', type: operation.parameterType },
{ name: 'context', type: 'SlushyContext<TContext>' },
],
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export class ResourcesConfigurationFactory {
openApiConstantSourceFile: string,
tsFile: TSFile
) {
const resourcesConfigurationClassBuilder = new TSClassBuilder('ResourcesConfiguration')
const resourcesConfigurationInterfaceBuilder = new TSInterfaceBuilder('Config')
const resourcesConfigurationClassBuilder = new TSClassBuilder('ResourcesConfiguration', 'TContext')
const resourcesConfigurationInterfaceBuilder = new TSInterfaceBuilder('Config', 'TContext')

let bindings: string[] = []
for (const { resourceType, resourceRouterType } of applicationResourceDescriptions) {
Expand All @@ -55,16 +55,16 @@ export class ResourcesConfigurationFactory {

resourcesConfigurationInterfaceBuilder.addProperty({
name: resourceType,
type: resourceType,
type: `${resourceType}<TContext>`,
})
}
tsFile.addSourceText(resourcesConfigurationInterfaceBuilder.build())

resourcesConfigurationClassBuilder.addConstructorParameter({ name: 'config', type: 'Config' })
resourcesConfigurationClassBuilder.addConstructorParameter({ name: 'config', type: 'Config<TContext>' })
resourcesConfigurationClassBuilder.addMethod({
name: 'configure',
async: true,
parameters: [{ name: 'router', type: 'SlushyRouter' }],
parameters: [{ name: 'router', type: 'SlushyRouter<TContext>' }],
returnType: 'Promise<void>',
body: bindings.join('\n'),
})
Expand Down
7 changes: 5 additions & 2 deletions codegen/src/generators/ResourcesGenerator/RouterFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ export class RouterFactory {
}

const resourceRouterName = `${resourceType}Router`
const routerClassBuilder = new TSClassBuilder(resourceRouterName)
const routerClassBuilder = new TSClassBuilder(resourceRouterName, 'TContext')
routerClassBuilder.addMethod({
name: 'bindRoutes',
async: true,
parameters: [{ name: 'router', type: 'SlushyRouter' }, { name: 'resource', type: resourceType }],
parameters: [
{ name: 'router', type: 'SlushyRouter<TContext>' },
{ name: 'resource', type: `${resourceType}<TContext>` },
],
returnType: 'Promise<void>',
body: statements.join('\n'),
})
Expand Down
6 changes: 4 additions & 2 deletions codegen/src/typescript/TSClassBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class TSClassBuilder {
private readonly properties: TSClassProperty[] = []
private readonly methods: TSClassMethod[] = []

public constructor(public readonly className: string) {}
public constructor(public readonly className: string, private readonly generics?: string) {}

public addConstructorParameter(param: TSClassProperty) {
this.parameters.push(param)
Expand All @@ -31,7 +31,9 @@ export class TSClassBuilder {

public build() {
return `
export class ${this.className}${this.extend ? ` extends ${this.extend.className}` : ''} {
export class ${this.className}${this.generics ? `<${this.generics}>` : ''}${
this.extend ? ` extends ${this.extend.className}` : ''
} {
${this.properties
.map(
prop =>
Expand Down
3 changes: 2 additions & 1 deletion example/__tests__/PetsResource.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import request from 'supertest'
import { SlushyFactory } from '../src/SlushyFactory'
import { Slushy } from '@slushy/server'
import { Context } from './Context'

describe('PetsResource', () => {
let slushy: Slushy
let slushy: Slushy<Context>

beforeEach(async () => {
slushy = await SlushyFactory.create({
Expand Down
1 change: 1 addition & 0 deletions example/src/Context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface Context {}
8 changes: 6 additions & 2 deletions example/src/PetsResourceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
} from './generated/resources/PetsResource'
import { Pet } from './generated/types'
import { SlushyContext } from '@slushy/server'
import { Context } from './Context'

export class PetsResourceImpl implements PetsResource<{}> {
export class PetsResourceImpl implements PetsResource<Context> {
private pets: Pet[] = [{ id: 1, name: 'Pet 1' }]

public async getPets(): Promise<GetPetsResponse> {
Expand All @@ -41,7 +42,10 @@ export class PetsResourceImpl implements PetsResource<{}> {
return new CreatePetOK(pet)
}

public async uploadPetPicture(_params: UploadPetPictureParams, _context: SlushyContext<{}>): Promise<UploadPetPictureResponse> {
public async uploadPetPicture(
_params: UploadPetPictureParams,
_context: SlushyContext<Context>
): Promise<UploadPetPictureResponse> {
return new UploadPetPictureOK()
}
}
8 changes: 6 additions & 2 deletions example/src/SlushyFactory.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Slushy, SlushyConfig, SlushyPlugins } from '@slushy/server'
import { PetsResourceImpl } from './PetsResourceImpl'
import { ResourcesConfiguration } from './generated/ResourcesConfiguration'
import { Context } from './Context'

export class SlushyFactory {
public static async create(config: Partial<SlushyConfig> & Partial<SlushyPlugins> = {}) {
const slushy = await Slushy.create({
public static async create(
config: Partial<SlushyConfig<Context>> & Partial<SlushyPlugins> = {}
): Promise<Slushy<Context>> {
const slushy = await Slushy.create<Context>({
contextFactory: ctx => ({ ...ctx, context: {} }),
...config,
resourceConfiguration: new ResourcesConfiguration({
PetsResource: new PetsResourceImpl(),
Expand Down
9 changes: 4 additions & 5 deletions server/src/ContextFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import { SlushyProps } from './SlushyProps'
import { OpenApiBridge } from './ServerImpl'
import { Logger } from './LoggerFactory'

export class ContextFactory<TContext = {}> {
export class ContextFactory<TContext> {
public constructor(private readonly openApiBridge = new OpenApiBridge()) {}

public async buildContext(
req: SlushyRequest,
res: SlushyResponse,
requestId: string,
logger: Logger,
props: SlushyProps
props: SlushyProps<TContext>
): Promise<SlushyContext<TContext>> {
// FIXME: Add TContext
const context: SlushyContext<TContext> = {
const partialContext: SlushyContext<TContext> = {
req,
res,
requestId,
Expand All @@ -25,8 +25,7 @@ export class ContextFactory<TContext = {}> {
pathItemObject: this.getPathItemObject(req, props.openApi),
operationObject: this.getOperationObject(req, props.openApi),
}

return context
return props.contextFactory ? props.contextFactory(partialContext as SlushyContext<any>) : partialContext
}

protected getPathItemObject(req: SlushyRequest, openApi: OpenAPIV3.Document): OpenAPIV3.PathItemObject {
Expand Down
2 changes: 1 addition & 1 deletion server/src/RequestParametersExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BadRequestError } from './errors/BadRequestError'
import Ajv from 'ajv'
import { isReferenceObject } from './isReferenceObject'

export class RequestParametersExtractor<TContext = {}> {
export class RequestParametersExtractor<TContext> {
private readonly validator = new Ajv({ allErrors: true })

/**
Expand Down
8 changes: 4 additions & 4 deletions server/src/Slushy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { SlushyPlugins } from './SlushyPlugins'
import { DefaultLoggerFactory } from './LoggerFactory'
import { OpenAPIV3 } from 'openapi-types'

export class Slushy {
export class Slushy<TContext> {
public constructor(
public readonly props: Readonly<SlushyProps>,
public readonly props: Readonly<SlushyProps<TContext>>,
public readonly app: SlushyApplication = SlushyApplicationFactory.create(),
public readonly router: SlushyRouter = new SlushyRouter(props, app)
public readonly router: SlushyRouter<TContext> = new SlushyRouter(props, app)
) {}

public async start(port: number) {
Expand All @@ -22,7 +22,7 @@ export class Slushy {
await this.props.resourceConfiguration.configure(this.router)
}

public static async create(config: SlushyConfig & Partial<SlushyPlugins>) {
public static async create<TContext>(config: SlushyConfig<TContext> & Partial<SlushyPlugins>) {
const openApi = JSON.parse(config.resourceConfiguration.getOpenApiSchema()) as OpenAPIV3.Document
const slushy = new Slushy({
openApi,
Expand Down
6 changes: 4 additions & 2 deletions server/src/SlushyConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SlushyResourceConfiguration } from './SlushyResourceConfiguration'
import { SlushyContext } from './SlushyContext'

export interface SlushyConfig {
resourceConfiguration: SlushyResourceConfiguration
export interface SlushyConfig<TContext> {
resourceConfiguration: SlushyResourceConfiguration<TContext>
hostname?: string
contextFactory?: (partialContext: SlushyContext<undefined>) => SlushyContext<TContext>
}
Loading

0 comments on commit 8aad61e

Please sign in to comment.