feathersjs-ecosystem / feathers-swagger

Add documentation to your FeatherJS services and feed them to Swagger UI.
MIT License
225 stars 63 forks source link

Define model in json file #155

Closed bwgjoseph closed 5 years ago

bwgjoseph commented 5 years ago

Hi,

I am trying out schemasGenerator and it works just fine if I define everything without a standalone json file as below.

video.server.ts

// An extract
const cService = createService(options);

  cService.model = {
    type: 'object',
    required: [
      'id',
    ],
    properties: {
      id: { type: 'number' },
      b: { type: 'string' },
      start_date: {
        $ref: '#/components/schemas/ISODateFormat'
      },
      c: { type: 'number' },
      d: { type: 'number' },
      e: { type: 'string' }
    },
  };

  cService.docs = {
    description: 'Service to manage video',
    multi: ['create'],
  };

app.ts

app.configure(swagger({
  openApiVersion: 3,
  idType: 'string',
  docsPath: '/docs',
  docsJsonPath: "/jsondocs",
  uiIndex: true,
  specs: {
    info: {
      title: 'API',
      description: 'API',
      version: '1.0.0',
    },
    servers: [{
      url: 'http://localhost:3030',
      description: 'Production server (uses live data)'
    },
    {
      url: 'http://sandbox-api.example.com:8443/v1',
      description: 'Sandbox server (uses test data)'
    }
    ],
    components: {
      schemas: {
        ISODateFormat: {
          description: 'ISO format date-time.',
          example: '2018-01-01T01:01:01.001Z',
          format: 'date-time',
          readOnly: true,
          type: 'string'
        },
      }
    }
  },
  defaults: {
    schemasGenerator(service, model, modelName) {
      return {
        [model]: service.model,
        [`${model}_list`]: {
          title: `${modelName} list`,
          type: 'array',
          items: { $ref: `#/components/schemas/${model}` }
        },
      };
    }
  }
}));

In the GET path portion of the UI

image

However, if I extract the definition of cService.model into a .json file by itself as such..

video.json

{
  "type": "object",
  "required": [
    "id"
  ],
  "properties": {
    "id": {
      "type": "number"
    },
    "b": {
      "type": "string"
    },
    "start_date": {
      "$ref": "#/components/schemas/ISODateFormat"
    },
    "c": {
      "type": "number"
    },
    "d": {
      "type": "number"
    },
    "e": {
      "type": "string"
    }
  }
}

Update video.service.ts to

import * as video_model from video.json

  const cService = createService(options);

  cService.model = video_model;

  cService.docs = {
    description: 'Service to manage video',
    multi: ['create'],
  };

The result will become like this.

image

Is it because I can't import and define a standalone json? Or did I do it wrongly?

Any idea what is causing this?

Thanks!

Mairu commented 5 years ago

Importing the JSON from an external file should lead to exactly the same behavior.

Depending on the TypeScript Version you use and how you configured it to import JSON files, it could be that you imported the JSON in a wrong way. (I guess this should be your problem). For the different approaches read https://hackernoon.com/import-json-into-typescript-8d465beded79

bwgjoseph commented 5 years ago

Hi @Mairu,

Thank you once again for help. Am currently using typescript v3.5.2, and there is no need to define resolveJsonModule: true in tsconfig.json.

I just have to import using import video_model from video.json without * as and it works just fine.


Do you have any idea why start_date is not showing in the filter option?

image


And lastly, another related question. As you can see that I am currently defining model in video.service.ts as such.

import video_model from './video.json'

cService.model = video_model;

The definition for the model, and the JSON schema definition in video.schema.ts generated by feathers-plus-cli is almost similar yet with a small differences in certain definition.

// Define the model using JSON-schema
let schema = {
  // !<DEFAULT> code: schema_header
  title: 'Video',
  description: 'Video database.',
  // !end
  // !code: schema_definitions // !end

  // Required fields.
  required: [
    'id',
    // !code: schema_required // !end
  ],
  // Fields with unique values.
  uniqueItemProperties: [
    // !code: schema_unique // !end
  ],

  // Fields in the model.
 properties: {
    // !code: schema_properties
    id: { type: 'number' },
    b: { type: 'string' },
    // doesn't work as well
    // start_date: { $ref: '../../refs/common.json#/definitions/ISODateFormat' },
    // throws 'error: Page not found {"type":"FeathersError","name":"NotFound","code":404,"className":"not-found","data":{"url":"/refs/common.json"},"errors":{}}' in feathers console
    start_date: { $ref: 'common.json#/definitions/ISODateFormat' },
    // throws 'error: Page not found {"type":"FeathersError","name":"NotFound","code":404,"className":"not-found","data":{"url":"/common.json"},"errors":{}}' in feathers console
    c: { type: 'number' },
    d: { type: 'number' },
    e: { type: 'string' }
    // !end
  },
  // !code: schema_more // !end
};

So I'm trying to re-use the schema defined in video.schema.ts instead of having to re-define another video.json which makes it difficult to maintain. So this is what I did.

import v from './video.schema';

cService.model = v.schema;

Works great except that it is not able to ref to ISODateFormat. My guess is due to openApi v3, the definition changed to component/schema.

image

*Note that ISODateFormat is ref to common.json for the definition.

*Update: I made a change to set start_date: { $ref: '#/components/schemas/ISODateFormat' }, and it works for swagger but I suspect that it will break for the application generator since JSON Schema probably don't understand #/components/schemas/.

*Update 2: feathers-plus-cli indeeds break, as it is not able to resolve the schema correctly when I changed to use #/components/schemas/ and then generate the service again. Any workaround?

Is there any way to achieve this? Re-using video.schema.ts instead of having to define another video.json for the cService.model.

Thanks!

Mairu commented 5 years ago

You can also use external refs in swagger. If you have a ref that refers to an external file, this file has to be reachable under the same path used for docsJsonPath (or docsPath if docsJsonPath is not used). The "internal" path in the external path can be definitions or anything else.

So the important thing is, that if you use /jsondocs as the docsJsonPath, that /common.json is pointing to your common.json. If you you use /docs/app.json it would be /docs/common.json.

bwgjoseph commented 5 years ago

Hi,

Sorry, but I don't quite get what you meant but...

This is my current directory structure.

image

So in app.ts, I have the following:

app.configure(swagger({
  openApiVersion: 3,
  idType: 'string',
  docsPath: '/docs',
  docsJsonPath: "/jsondocs",
....

The only way that I got it working is by copying common.json to /public directory so that it can be served and accessed. Otherwise, if it is inside src/refs/common.json, it is not able to resolve, and will prompt error when I access swagger-ui docs.

Is that the right way?

And even though there is no error, I noticed by that doing so, the filter portion seem to not have the object as seen (in the previous post). But I am still able to perform the get query. Not sure if I did any steps wrong here.

image

Mairu commented 5 years ago

You can serve the common.json with a custom path by using express directly.

In app.ts use

  app.route('/common.json').get((req, res) => {
    res.sendFile(path.join(__dirname, 'refs', 'common.json')); // could be different if you compile ts to js into another directory without copying the json files
  });

That the referenced parameter is not shown for filter parameters is because of the readOnly flag of the ISODateFormat.

bwgjoseph commented 5 years ago

Hi,

After playing around with it, I learnt something new. For the benefit of those that come after.

All the fields in filter parameter wasn't showing wasn't due to readOnly flag in ISODateFormat. It wasn't showing because I did not set the type: 'object in video.schema.ts.

I added the custom route to serve common.json.

Updated video.schema.ts

let schema = {
  // !<DEFAULT> code: schema_header
  title: 'Video',
  description: 'Video database.',
  // !end
  // !code: schema_definitions // !end
  // Need to set this to show in filter parameter
  type: 'object',
  // Required fields.
  required: [
    // !code: schema_required // !end
  ],
  // Fields with unique values.
  uniqueItemProperties: [
    // !code: schema_unique // !end
  ],

  // Fields in the model.
  properties: {
    // !code: schema_properties
    id: { type: 'number' },
    b: { type: 'string' },
    start_date: { $ref: 'common.json#/definitions/ISODateFormat' },
    c: { type: 'number' },
    d: { type: 'number' },
    e: { type: 'string' }
    // !end
  },
  // !code: schema_more // !end
};

Toggling readOnly flag of a field will only show/hide in the filter parameter.

readOnly: false

image

readOnly: true. start_date will no longer be shown.

image

Thank you once again, @Mairu, for assisting me.