const fs = require('fs')
const Path = require('path')
const NCC = require('@sevenbitbyte/ncc')
//const NCC = require('@zeit/ncc')
const Hoek = require('@hapi/hoek')
const {JSONPath} = require('jsonpath-plus')
const gitRepoInfo = require('git-repo-info')
const BouncerDb = require('@dataparty/bouncer-db')
const mongoose = BouncerDb.mongoose()
const { build } = require('joi')
const debug = require('debug')('dataparty.service.IService')
module.exports = class IService {
/**
* A service with schema, documents, endpoints, middleware and tasks.
* Provide either paths to source files for compilation or provided a
* pre-built service to import.
*
* @class module:Service.IService
* @link module:Service
*
* @param {*} options.name
* @param {*} options.version
* @param {*} options.githash
* @param {*} options.branch
* @param {*} build
*/
constructor({
name, version, githash='', branch=''
}, build){
this.constructors = {
schemas: {},
documents: {},
endpoints: {},
middleware: {
pre: {},
post: {}
},
tasks: {}
}
this.middleware_order = {
pre: [],
post: []
}
this.sources = {
schemas: {},
documents: {},
endpoints: {},
middleware: {
pre: {},
post: {}
},
tasks: {}
}
this.compiled = {
package: { name, version, githash, branch },
schemas: {
IndexSettings: {},
JSONSchema: [],
Permissions: {}
},
documents: {},
endpoints: {},
middleware: {
pre: {},
post: {}
},
middleware_order: {
pre: [],
post: []
},
tasks: {}
}
this.compileSettings = {
// provide a custom cache path or disable caching
cache: false,
// externals to leave as requires of the build
externals: ['debug', '@dataparty/crypto', '@dataparty/tasker', 'joi', '@hapi/hoek'],
// directory outside of which never to emit assets
//filterAssetBase: process.cwd(), // default
minify: false, // default
sourceMap: true, // default
//sourceMapBasePrefix: '../', // default treats sources as output-relative
// when outputting a sourcemap, automatically include
// source-map-support in the output file (increases output by 32kB).
sourceMapRegister: false, // default
watch: false, // default
v8cache: false, // default
quiet: false, // default
debugLog: false, // default
//target: 'es2015'
esm: false,
moduleType: 'self',
libraryName: 'Lib'
}
if(build){
this.importBuild(build)
}
}
/**
* Import a pre-build service
* @method module:Service.IService.importBuild
* @param {Object} buildOutput
*/
importBuild(buildOutput){
this.compiled = buildOutput
}
/**
* Add a dataparty schema implementation to the service
* @method module:Service.IService.addSchema
* @param {module:Service.IService} schema_path
*/
addSchema(schema_path){
debug('addSchema', schema_path)
const schema = require(schema_path)
const name = schema.Type
this.sources.schemas[name] = schema_path
this.constructors.schemas[name] = schema
}
/**
* Add a document class implementation to the service
* @method module:Service.IService.addDocument
* @param {string} document_path
*/
addDocument(document_path){
debug('addDocument', document_path)
const document = require(document_path)
const name = document.DocumentSchema
this.sources.documents[name] = document_path
this.constructors.documents[name] = document
}
/**
* Add a dataparty endpoint to the service by pather
* @method module:Service.IService.addEndpoint
* @param {string} endpoint_path
*/
addEndpoint(endpoint_path){
debug('addEndpoint', endpoint_path)
const endpoint = require(endpoint_path)
const name = endpoint.Name
this.sources.endpoints[name] = endpoint_path
this.constructors.endpoints[name] = endpoint
}
/**
* Add a middleware to this service
* @method module:Service.IService.addEndpoint
* @param {string} middleware_path
*/
addMiddleware(middleware_path){
debug('addMiddleware',middleware_path)
const middleware = require(middleware_path)
const name = middleware.Name
const type = middleware.Type
debug('addMiddleware',type,name)
this.middleware_order[type].push(name)
this.sources.middleware[type][name] = middleware_path
this.constructors.middleware[type][name] = middleware
}
/**
* Add a `tasker` task implementation to the service
* @method module:Service.IService.addTask
* @see https://github.com/datapartyjs/tasker
* @param {string} task_path
*/
addTask(task_path){
debug('addTask', task_path)
const TaskClass = require(task_path)
const name = TaskClass.Name
this.sources.tasks[name] = task_path
this.constructors.tasks[name] = TaskClass
}
/**
* Compile a service. This will build two output files, one for host usage `-service.json`
* and another for client usage `-schema.json`.
* @async
* @method module:Service.IService.compile
* @param {string} outputPath Path where the built service should be written
* @param {boolean} writeFile When true, files will be written. Defaults to `true`
* @returns
*/
async compile(outputPath, writeFile=true){
if(!outputPath){
throw new Error('no output path')
}
const info = gitRepoInfo(Path.dirname(outputPath))
this.compiled.package.githash = info.sha
this.compiled.package.branch = info.branch
debug('compiling sources',this.sources)
await Promise.all([
this.compileMiddleware('pre'),
this.compileMiddleware('post'),
this.compileList('documents'),
this.compileList('endpoints'),
this.compileList('tasks'),
this.compileSchemas()
])
this.compiled.middleware_order = this.middleware_order
this.compiled.compileSettings = this.compileSettings
if(writeFile){
const buildOutput = outputPath+'/'+ this.compiled.package.name.replace('/', '-') +'.dataparty-service.json'
fs.writeFileSync(buildOutput, JSON.stringify(this.compiled, null,2))
const schemaOutput = outputPath+'/'+ this.compiled.package.name.replace('/', '-') +'.dataparty-schema.json'
fs.writeFileSync(schemaOutput, JSON.stringify({
package: this.compiled.package,
...this.compiled.schemas
}, null, 2))
}
return this.compiled
}
async compileList(field, outputPath){
// Build file list
debug('compileList',field)
for(const name in this.sources[field]){
debug('\r', field, name)
const buildPath = !outputPath ? '' : Path.join(outputPath, field+'-'+name)
const build = await this.compileFileTo(this.sources[field][name], buildPath)
this.compiled[field][name] = build
}
}
async compileMiddleware(type,outputPath){
// Build pre middleware
for(const name of this.middleware_order[type]){
debug('\r', type, name)
const buildPath = !outputPath ? '' : Path.join(outputPath, 'middleware-'+type+'-'+name)
const build = await this.compileFileTo(this.sources.middleware[type][name], buildPath)
this.compiled.middleware[type][name] = build
}
}
async compileFileTo(input, output){
const { code, map, assets } = await NCC(input, this.compileSettings)
debug('compileFileTo', input, '->', output)
debug('\t','code length', Math.round(code.length/1024), 'KB')
if(output){
fs.writeFileSync(output+'.js', code)
}
return {code, map, assets}
}
async compileSchemas(buildTypeScript=false){
debug('compileSchema')
for(let key in this.constructors.schemas){
debug('\tcompiling', key)
const model = this.constructors.schemas[key]
let schema = mongoose.Schema(model.Schema)
schema = model.setupSchema(schema)
let jsonSchema = schema.jsonSchema()
jsonSchema.title = model.Type
this.compiled.schemas.Permissions[model.Type] = await model.permissions()
this.compiled.schemas.JSONSchema.push(jsonSchema)
debug('\t','type',model.Type)
let indexed = JSONPath({
path: '$..options.index',
json: schema.paths,
resultType: 'pointer'
}).map(p=>{return p.split('/')[1]})
debug('\t\tindexed', indexed)
let unique = JSONPath({
path: '$..options.unique',
json: schema.paths,
resultType: 'pointer'
}).map(p=>{
debug(typeof p)
if(typeof p == 'string'){
return p.split('/')[1]
}
return p
})
debug('\t\tunique', unique)
debug('\t\tindexes', schema._indexes)
let compoundIndices = {
indices: Hoek.reach(schema, '_indexes.0.0'),
unique: Hoek.reach(schema, '_indexes.0.1.unique')
}
this.compiled.schemas.IndexSettings[model.Type] = {
indices: indexed,
unique,
compoundIndices
}
if(buildTypeScript){
throw 'implementation removed'
/*
const json2ts = require('json-schema-to-typescript')
const tsWrite = json2ts.compile(jsonSchema).then( ts=>{
tsOutput[model.Type] = ts
})
tsWrites.push(tsWrites)
*/
}
}
if(buildTypeScript){ await Promise.all(tsWrites) }
}
}