feat: webshipper (#118)
* register fulfillment providers as singletons * fix: adds subscribers/api endpoints and webhook handlers * Adds readme * Allow document attachments to orders: * chore: gitignore utils * chore: rm compiled files
This commit is contained in:
13
packages/medusa-fulfillment-webshipper/.babelrc
Normal file
13
packages/medusa-fulfillment-webshipper/.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/medusa-fulfillment-webshipper/.eslintrc
Normal file
9
packages/medusa-fulfillment-webshipper/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
14
packages/medusa-fulfillment-webshipper/.gitignore
vendored
Normal file
14
packages/medusa-fulfillment-webshipper/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/utils
|
||||
|
||||
10
packages/medusa-fulfillment-webshipper/.npmignore
Normal file
10
packages/medusa-fulfillment-webshipper/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
/src
|
||||
|
||||
|
||||
7
packages/medusa-fulfillment-webshipper/.prettierrc
Normal file
7
packages/medusa-fulfillment-webshipper/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
15
packages/medusa-fulfillment-webshipper/README.md
Normal file
15
packages/medusa-fulfillment-webshipper/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# medusa-fulfillment-webshipper
|
||||
|
||||
Adds Webshipper as a fulfilment provider in Medusa Commerce.
|
||||
|
||||
On each new fulfillment an order is created in Webshipper. The plugin listens for shipment events and updated the shipment accordingly.
|
||||
A webhook listener is exposed at `/webshipper/shipments` to listen for shipment creations. You must create this webhook in Webshipper to have Medusa listen for shipment events.
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
account: [your webshipper account] (required)
|
||||
api_token: [a webshipper api token] (required)
|
||||
order_channel_id: [the channel id to register orders on] (required)
|
||||
webhook_secret: [the webhook secret used to listen for shipments] (required)
|
||||
```
|
||||
1
packages/medusa-fulfillment-webshipper/index.js
Normal file
1
packages/medusa-fulfillment-webshipper/index.js
Normal file
@@ -0,0 +1 @@
|
||||
// noop
|
||||
41
packages/medusa-fulfillment-webshipper/package.json
Normal file
41
packages/medusa-fulfillment-webshipper/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "medusa-fulfillment-webshipper",
|
||||
"version": "1.0.0",
|
||||
"description": "Webshipper Fulfillment provider for Medusa",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-fulfillment-webshipper"
|
||||
},
|
||||
"author": "Sebastian Rindom",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d .",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"medusa-interfaces": "1.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.20.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^1.0.10"
|
||||
},
|
||||
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408"
|
||||
}
|
||||
73
packages/medusa-fulfillment-webshipper/src/api/index.js
Normal file
73
packages/medusa-fulfillment-webshipper/src/api/index.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Router } from "express"
|
||||
import bodyParser from "body-parser"
|
||||
import crypto from "crypto"
|
||||
import cors from "cors"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
|
||||
export default (rootDirectory) => {
|
||||
const app = Router()
|
||||
|
||||
const { configModule } = getConfigFile(rootDirectory, "medusa-config")
|
||||
const { projectConfig } = configModule
|
||||
|
||||
const corsOptions = {
|
||||
origin: projectConfig.store_cors.split(","),
|
||||
credentials: true,
|
||||
}
|
||||
|
||||
app.options("/webshipper/drop-points/:rate_id", cors(corsOptions))
|
||||
app.get(
|
||||
"/webshipper/drop-points/:rate_id",
|
||||
cors(corsOptions),
|
||||
async (req, res) => {
|
||||
const { rate_id } = req.params
|
||||
const { address_1, postal_code, country_code } = req.query
|
||||
|
||||
try {
|
||||
const webshipperService = req.scope.resolve(
|
||||
"webshipperFulfillmentService"
|
||||
)
|
||||
|
||||
const dropPoints = await webshipperService.retrieveDropPoints(
|
||||
rate_id,
|
||||
postal_code,
|
||||
country_code,
|
||||
address_1
|
||||
)
|
||||
|
||||
res.json({
|
||||
drop_points: dropPoints,
|
||||
})
|
||||
} catch (err) {
|
||||
res.json({ drop_points: [] })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
app.post(
|
||||
"/webshipper/shipments",
|
||||
bodyParser.raw({ type: "application/vnd.api+json" }),
|
||||
async (req, res) => {
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
const logger = req.scope.resolve("logger")
|
||||
|
||||
const secret = `da791d87513eb091640f9fb6c4b94384`
|
||||
const hmac = crypto.createHmac("sha256", secret)
|
||||
const digest = hmac.update(req.body).digest("base64")
|
||||
const hash = req.header("x-webshipper-hmac-sha256")
|
||||
|
||||
if (hash === digest) {
|
||||
eventBus.emit("webshipper.shipment", {
|
||||
headers: req.headers,
|
||||
body: JSON.parse(req.body),
|
||||
})
|
||||
} else {
|
||||
logger.warn("Webshipper webhook could not be authenticated")
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import { FulfillmentService } from "medusa-interfaces"
|
||||
import Webshipper from "../utils/webshipper"
|
||||
|
||||
class WebshipperFulfillmentService extends FulfillmentService {
|
||||
static identifier = "webshipper"
|
||||
|
||||
constructor({ logger, orderService }, options) {
|
||||
super()
|
||||
|
||||
this.logger_ = logger
|
||||
this.orderService_ = orderService
|
||||
this.options_ = options
|
||||
this.client_ = new Webshipper({
|
||||
account: this.options_.account,
|
||||
token: this.options_.api_token,
|
||||
})
|
||||
}
|
||||
|
||||
registerInvoiceGenerator(service) {
|
||||
if (typeof service.createInvoice === "function") {
|
||||
this.invoiceGenerator_ = service
|
||||
}
|
||||
}
|
||||
|
||||
async getFulfillmentOptions() {
|
||||
const rates = await this.client_.shippingRates.list()
|
||||
|
||||
return rates.data.map((r) => ({
|
||||
id: r.attributes.name,
|
||||
webshipper_id: r.id,
|
||||
name: r.attributes.name,
|
||||
require_drop_point: r.attributes.require_drop_point,
|
||||
carrier_id: r.attributes.carrier_id,
|
||||
}))
|
||||
}
|
||||
|
||||
async validateFulfillmentData(data, _) {
|
||||
if (data.require_drop_point) {
|
||||
if (!data.drop_point_id) {
|
||||
throw new Error("Must have drop point id")
|
||||
} else {
|
||||
// TODO: validate that the drop point exists
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
async validateOption(data) {
|
||||
const rate = await this.client_.shippingRates
|
||||
.retrieve(data.webshipper_id)
|
||||
.catch(() => undefined)
|
||||
return !!rate
|
||||
}
|
||||
|
||||
canCalculate() {
|
||||
// Return whether or not we are able to calculate dynamically
|
||||
return false
|
||||
}
|
||||
|
||||
calculatePrice() {
|
||||
// Calculate prices
|
||||
}
|
||||
|
||||
async createOrder(methodData, fulfillmentItems, fromOrder) {
|
||||
const existing =
|
||||
fromOrder.metadata && fromOrder.metadata.webshipper_order_id
|
||||
|
||||
let webshipperOrder
|
||||
if (existing) {
|
||||
webshipperOrder = await this.client_.orders.retrieve(existing)
|
||||
}
|
||||
|
||||
if (!webshipperOrder) {
|
||||
let invoice
|
||||
if (this.invoiceGenerator_) {
|
||||
const base64Invoice = await this.invoiceGenerator_.createInvoice(
|
||||
fromOrder,
|
||||
fulfillmentItems
|
||||
)
|
||||
|
||||
invoice = await this.client_.documents
|
||||
.create({
|
||||
type: "documents",
|
||||
attributes: {
|
||||
document_size: this.options_.document_size || "A4",
|
||||
document_format: "PDF",
|
||||
base64: base64Invoice,
|
||||
document_type: "invoice",
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
const { shipping_address } = fromOrder
|
||||
const newOrder = {
|
||||
type: "orders",
|
||||
attributes: {
|
||||
status: "pending",
|
||||
ext_ref: `${fromOrder._id}.${fromOrder.fulfillments.length}`,
|
||||
visible_ref: `${fromOrder.display_id}-${
|
||||
fromOrder.fulfillments.length + 1
|
||||
}`,
|
||||
order_lines: fulfillmentItems.map((item) => {
|
||||
return {
|
||||
ext_ref: item._id,
|
||||
sku: item.content.variant.sku,
|
||||
description: item.title,
|
||||
quantity: item.quantity,
|
||||
country_of_origin:
|
||||
item.content.variant.metadata &&
|
||||
item.content.variant.metadata.origin_country,
|
||||
tarif_number:
|
||||
item.content.variant.metadata &&
|
||||
item.content.variant.metadata.hs_code,
|
||||
unit_price: item.content.unit_price,
|
||||
}
|
||||
}),
|
||||
delivery_address: {
|
||||
att_contact: `${shipping_address.first_name} ${shipping_address.last_name}`,
|
||||
address_1: shipping_address.address_1,
|
||||
address_2: shipping_address.address_2,
|
||||
zip: shipping_address.postal_code,
|
||||
city: shipping_address.city,
|
||||
country_code: shipping_address.country_code,
|
||||
state: shipping_address.province,
|
||||
phone: shipping_address.phone,
|
||||
email: fromOrder.email,
|
||||
},
|
||||
currency: fromOrder.currency_code,
|
||||
},
|
||||
relationships: {
|
||||
order_channel: {
|
||||
data: {
|
||||
id: this.options_.order_channel_id,
|
||||
type: "order_channels",
|
||||
},
|
||||
},
|
||||
shipping_rate: {
|
||||
data: {
|
||||
id: methodData.webshipper_id,
|
||||
type: "shipping_rates",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if (methodData.require_drop_point) {
|
||||
newOrder.attributes.drop_point = {
|
||||
drop_point_id: methodData.drop_point_id,
|
||||
name: methodData.drop_point_name,
|
||||
zip: methodData.drop_point_zip,
|
||||
address_1: methodData.drop_point_address_1,
|
||||
city: methodData.drop_point_city,
|
||||
country_code: methodData.drop_point_country_code,
|
||||
}
|
||||
}
|
||||
if (invoice) {
|
||||
newOrder.relationships.documents = {
|
||||
data: [
|
||||
{
|
||||
id: invoice.data.id,
|
||||
type: invoice.data.type,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return this.client_.orders
|
||||
.create(newOrder)
|
||||
.then((result) => {
|
||||
return result.data
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger_.warn(err.response)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async handleWebhook(_, body) {
|
||||
const wsOrder = await this.retrieveRelationship(
|
||||
body.data.relationships.order
|
||||
)
|
||||
if (wsOrder.data.attributes.ext_ref) {
|
||||
const trackingNumbers = body.data.attributes.tracking_links.map(
|
||||
(l) => l.number
|
||||
)
|
||||
const [orderId, fulfillmentIndex] = wsOrder.data.attributes.ext_ref.split(
|
||||
"."
|
||||
)
|
||||
|
||||
const order = await this.orderService_.retrieve(orderId)
|
||||
const fulfillment = order.fulfillments[fulfillmentIndex]
|
||||
if (fulfillment) {
|
||||
await this.orderService_.createShipment(
|
||||
order._id,
|
||||
fulfillment._id,
|
||||
trackingNumbers
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async retrieveDropPoints(id, zip, countryCode, address1) {
|
||||
const points = await this.client_
|
||||
.request({
|
||||
method: "POST",
|
||||
url: `/v2/drop_point_locators`,
|
||||
data: {
|
||||
data: {
|
||||
type: "drop_point_locators",
|
||||
attributes: {
|
||||
shipping_rate_id: id,
|
||||
delivery_address: {
|
||||
zip,
|
||||
country_code: countryCode,
|
||||
address_1: address1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ data }) => data)
|
||||
|
||||
return points.attributes.drop_points
|
||||
}
|
||||
|
||||
retrieveRelationship(relation) {
|
||||
const link = relation.links.related
|
||||
return this.client_.request({
|
||||
method: "GET",
|
||||
url: link,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default WebshipperFulfillmentService
|
||||
@@ -0,0 +1,15 @@
|
||||
class WebshipperSubscriber {
|
||||
constructor({ eventBusService, logger, webshipperFulfillmentService }) {
|
||||
this.webshipperService_ = webshipperFulfillmentService
|
||||
|
||||
eventBusService.subscribe("webshipper.shipment", this.handleShipment)
|
||||
}
|
||||
|
||||
handleShipment = async ({ headers, body }) => {
|
||||
return this.webshipperService_.handleWebhook(headers, body).catch((err) => {
|
||||
logger.warn(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default WebshipperSubscriber
|
||||
@@ -0,0 +1,97 @@
|
||||
import axios from "axios"
|
||||
|
||||
class Webshipper {
|
||||
constructor({ account, token }) {
|
||||
this.account_ = account
|
||||
this.token_ = token
|
||||
this.client_ = axios.create({
|
||||
baseURL: `https://${account}.api.webshipper.io`,
|
||||
headers: {
|
||||
"content-type": "application/vnd.api+json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
this.documents = this.buildDocumentEndpoints_()
|
||||
this.shippingRates = this.buildShippingRateEndpoints_()
|
||||
this.orders = this.buildOrderEndpoints_()
|
||||
this.shipments = this.buildShipmentEndpoints_()
|
||||
}
|
||||
|
||||
async request(data) {
|
||||
return this.client_(data).then(({ data }) => data)
|
||||
}
|
||||
|
||||
buildDocumentEndpoints_ = () => {
|
||||
return {
|
||||
create: async (data) => {
|
||||
const path = `/v2/documents`
|
||||
return this.client_({
|
||||
method: "POST",
|
||||
url: path,
|
||||
data: {
|
||||
data,
|
||||
},
|
||||
}).then(({ data }) => data)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildShippingRateEndpoints_ = () => {
|
||||
return {
|
||||
retrieve: async (id) => {
|
||||
const path = `/v2/shipping_rates/${id}`
|
||||
return this.client_({
|
||||
method: "GET",
|
||||
url: path,
|
||||
}).then(({ data }) => data)
|
||||
},
|
||||
list: async () => {
|
||||
const path = `/v2/shipping_rates`
|
||||
return this.client_({
|
||||
method: "GET",
|
||||
url: path,
|
||||
}).then(({ data }) => data)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildOrderEndpoints_ = () => {
|
||||
return {
|
||||
retrieve: async (id) => {
|
||||
const path = `/v2/orders/${id}`
|
||||
return this.client_({
|
||||
method: "GET",
|
||||
url: path,
|
||||
}).then(({ data }) => data)
|
||||
},
|
||||
create: async (data) => {
|
||||
const path = `/v2/orders`
|
||||
return this.client_({
|
||||
method: "POST",
|
||||
url: path,
|
||||
data: {
|
||||
data,
|
||||
},
|
||||
}).then(({ data }) => data)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildShipmentEndpoints_ = () => {
|
||||
return {
|
||||
create: async (data) => {
|
||||
const path = `/v2/shipments`
|
||||
return this.client_({
|
||||
method: "POST",
|
||||
url: path,
|
||||
data: {
|
||||
data,
|
||||
},
|
||||
}).then(({ data }) => data)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Webshipper
|
||||
@@ -15,12 +15,36 @@ class BaseFulfillmentService extends BaseService {
|
||||
return this.constructor.identifier
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a shipping option is created in Admin. The method should
|
||||
* return all of the options that the fulfillment provider can be used with,
|
||||
* and it is here the distinction between different shipping options are
|
||||
* enforced. For example, a fulfillment provider may offer Standard Shipping
|
||||
* and Express Shipping as fulfillment options, it is up to the store operator
|
||||
* to create shipping options in Medusa that can be chosen between by the
|
||||
* customer.
|
||||
*/
|
||||
getFulfillmentOptions() {}
|
||||
|
||||
/**
|
||||
* Called before a shipping method is set on a cart to ensure that the data
|
||||
* sent with the shipping method is valid. The data object may contain extra
|
||||
* data about the shipment such as an id of a drop point. It is up to the
|
||||
* fulfillment provider to enforce that the correct data is being sent
|
||||
* through.
|
||||
* @param {object} data - the data to validate
|
||||
* @param {object} cart - the cart to which the shipping method will be applied
|
||||
* @return {object} the data to populate `cart.shipping_methods.$.data` this
|
||||
* is usually important for future actions like generating shipping labels
|
||||
*/
|
||||
validateFulfillmentData(data, cart) {
|
||||
throw Error("validateFulfillmentData must be overridden by the child class")
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a shipping option is created in Admin. Use this to ensure
|
||||
* that a fulfillment option does in fact exist.
|
||||
*/
|
||||
validateOption(data) {
|
||||
throw Error("validateOption must be overridden by the child class")
|
||||
}
|
||||
@@ -29,7 +53,10 @@ class BaseFulfillmentService extends BaseService {
|
||||
throw Error("canCalculate must be overridden by the child class")
|
||||
}
|
||||
|
||||
calculatePrice(data) {
|
||||
/**
|
||||
* Used to calculate a price for a given shipping option.
|
||||
*/
|
||||
calculatePrice(data, cart) {
|
||||
throw Error("calculatePrice must be overridden by the child class")
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,7 @@ import { sync as existsSync } from "fs-exists-cached"
|
||||
* Registers all services in the services directory
|
||||
*/
|
||||
export default async ({ rootDirectory, container, app }) => {
|
||||
const { configModule, configFilePath } = getConfigFile(
|
||||
rootDirectory,
|
||||
`medusa-config`
|
||||
)
|
||||
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
|
||||
|
||||
if (!configModule) {
|
||||
return
|
||||
@@ -53,7 +50,7 @@ export default async ({ rootDirectory, container, app }) => {
|
||||
registerModels(pluginDetails, container)
|
||||
await registerServices(pluginDetails, container)
|
||||
registerMedusaApi(pluginDetails, container)
|
||||
registerApi(pluginDetails, app, rootDirectory)
|
||||
registerApi(pluginDetails, app, rootDirectory, container)
|
||||
registerCoreRouters(pluginDetails, container)
|
||||
registerSubscribers(pluginDetails, container)
|
||||
})
|
||||
@@ -143,12 +140,22 @@ function registerCoreRouters(pluginDetails, container) {
|
||||
/**
|
||||
* Registers the plugin's api routes.
|
||||
*/
|
||||
function registerApi(pluginDetails, app, rootDirectory = "") {
|
||||
function registerApi(pluginDetails, app, rootDirectory = "", container) {
|
||||
const logger = container.resolve("logger")
|
||||
logger.info(`Registering custom endpoints for ${pluginDetails.name}`)
|
||||
try {
|
||||
const routes = require(`${pluginDetails.resolve}/api`).default
|
||||
app.use("/", routes(rootDirectory))
|
||||
if (routes) {
|
||||
app.use("/", routes(rootDirectory))
|
||||
}
|
||||
return app
|
||||
} catch (err) {
|
||||
if (err.message !== `Cannot find module '${pluginDetails.resolve}/api'`) {
|
||||
logger.warn(
|
||||
`An error occured while registering customer endpoints for ${pluginDetails.name}`
|
||||
)
|
||||
logger.error(err.stack)
|
||||
}
|
||||
return app
|
||||
}
|
||||
}
|
||||
@@ -218,7 +225,7 @@ async function registerServices(pluginDetails, container) {
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
),
|
||||
).singleton(),
|
||||
[`fp_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
} else if (loaded.prototype instanceof FileService) {
|
||||
|
||||
Reference in New Issue
Block a user