{"version":3,"file":"apis.mjs","sources":["../../src/loaders/apis.ts"],"sourcesContent":["import { join, extname, basename } from 'path';\nimport fse, { existsSync } from 'fs-extra';\nimport _ from 'lodash';\nimport { strings, importDefault } from '@strapi/utils';\nimport { isEmpty } from 'lodash/fp';\nimport type { Core, Struct } from '@strapi/types';\nimport { getGlobalId, type ContentTypeDefinition } from '../domain/content-type';\n\ninterface API {\n  bootstrap: () => void | Promise<void>;\n  destroy: () => void | Promise<void>;\n  register: () => void | Promise<void>;\n  config: Record<string, unknown>;\n  routes: Record<string, Core.Router>;\n  controllers: Record<string, Core.Controller>;\n  services: Record<string, Core.Service>;\n  policies: Record<string, Core.Policy>;\n  middlewares: Record<string, Core.Middleware>;\n  contentTypes: Record<string, { schema: Struct.ContentTypeSchema }>;\n}\n\ninterface APIs {\n  [key: string]: API;\n}\n\nconst DEFAULT_CONTENT_TYPE = {\n  schema: {},\n  actions: {},\n  lifecycles: {},\n};\n\n// to handle names with numbers in it we first check if it is already in kebabCase\nconst normalizeName = (name: string) => (strings.isKebabCase(name) ? name : _.kebabCase(name));\n\nconst isDirectory = (fd: fse.Dirent) => fd.isDirectory();\nconst isDotFile = (fd: fse.Dirent) => fd.name.startsWith('.');\n\nexport default async function loadAPIs(strapi: Core.Strapi) {\n  if (!existsSync(strapi.dirs.dist.api)) {\n    return;\n  }\n\n  const apisFDs = await (await fse.readdir(strapi.dirs.dist.api, { withFileTypes: true }))\n    .filter(isDirectory)\n    .filter(_.negate(isDotFile));\n\n  const apis: APIs = {};\n\n  // only load folders\n  for (const apiFD of apisFDs) {\n    const apiName = normalizeName(apiFD.name);\n    const api = await loadAPI(apiName, join(strapi.dirs.dist.api, apiFD.name));\n\n    // @ts-expect-error TODO verify that it's a valid api, not missing bootstrap, register, and destroy\n    apis[apiName] = api;\n  }\n\n  validateContentTypesUnicity(apis);\n\n  for (const apiName of Object.keys(apis)) {\n    strapi.get('apis').add(apiName, apis[apiName]);\n  }\n}\n\nconst validateContentTypesUnicity = (apis: APIs) => {\n  const allApisSchemas = Object.values(apis).flatMap((api) => Object.values(api.contentTypes));\n\n  const names: string[] = [];\n  allApisSchemas.forEach(({ schema }) => {\n    if (schema.info.singularName) {\n      const singularName = _.kebabCase(schema.info.singularName);\n      if (names.includes(singularName)) {\n        throw new Error(`The singular name \"${schema.info.singularName}\" should be unique`);\n      }\n      names.push(singularName);\n    }\n\n    if (schema.info.pluralName) {\n      const pluralName = _.kebabCase(schema.info.pluralName);\n      if (names.includes(pluralName)) {\n        throw new Error(`The plural name \"${schema.info.pluralName}\" should be unique`);\n      }\n      names.push(pluralName);\n    }\n  });\n};\n\nconst loadAPI = async (apiName: string, dir: string) => {\n  const [index, config, routes, controllers, services, policies, middlewares, contentTypes] = (\n    await Promise.all([\n      loadIndex(dir),\n      loadDir(join(dir, 'config')),\n      loadDir(join(dir, 'routes')),\n      loadDir(join(dir, 'controllers')),\n      loadDir(join(dir, 'services')),\n      loadDir(join(dir, 'policies')),\n      loadDir(join(dir, 'middlewares')),\n      loadContentTypes(apiName, join(dir, 'content-types')),\n    ])\n  ).map((result) => result?.result);\n\n  return {\n    ...(index || {}),\n    config: config || {},\n    routes: routes || [],\n    controllers: controllers || {},\n    services: services || {},\n    policies: policies || {},\n    middlewares: middlewares || {},\n    contentTypes: contentTypes || {},\n  };\n};\n\nconst loadIndex = async (dir: string) => {\n  if (await fse.pathExists(join(dir, 'index.js'))) {\n    return loadFile(join(dir, 'index.js'));\n  }\n};\n\n// because this is async and its contents are dynamic, we must return it within an object to avoid a property called `then` being interpreted as a Promise\nconst loadContentTypes = async (apiName: string, dir: string) => {\n  if (!(await fse.pathExists(dir))) {\n    return;\n  }\n\n  const fds = await fse.readdir(dir, { withFileTypes: true });\n  const contentTypes: API['contentTypes'] = {};\n\n  // only load folders\n  for (const fd of fds) {\n    if (fd.isFile()) {\n      continue;\n    }\n\n    const contentTypeName = normalizeName(fd.name);\n    const loadedContentType = (await loadDir(join(dir, fd.name)))?.result;\n\n    if (isEmpty(loadedContentType) || isEmpty(loadedContentType.schema)) {\n      throw new Error(`Could not load content type found at ${dir}`);\n    }\n\n    const contentType = {\n      ...DEFAULT_CONTENT_TYPE,\n      ...loadedContentType,\n    } as ContentTypeDefinition;\n\n    Object.assign(contentType.schema, {\n      apiName,\n      collectionName: contentType.schema.collectionName || contentType.schema.info.singularName,\n      globalId: getGlobalId(contentType.schema),\n    });\n\n    contentTypes[normalizeName(contentTypeName)] = contentType;\n  }\n\n  return { result: contentTypes };\n};\n\n// because this is async and its contents are dynamic, we must return it within an object to avoid a property called `then` being interpreted as a Promise\nconst loadDir = async (dir: string) => {\n  if (!(await fse.pathExists(dir))) {\n    return;\n  }\n\n  const fds = await fse.readdir(dir, { withFileTypes: true });\n\n  const root: Record<string, unknown> = {};\n  for (const fd of fds) {\n    if (!fd.isFile() || extname(fd.name) === '.map') {\n      continue;\n    }\n\n    const key = basename(fd.name, extname(fd.name));\n\n    root[normalizeName(key)] = (await loadFile(join(dir, fd.name))).result;\n  }\n\n  return { result: root };\n};\n\n// because this is async and its contents are dynamic, we must return it as an array to avoid a property called `then` being interpreted as a Promise\nconst loadFile = async (file: string): Promise<{ result: unknown }> => {\n  const ext = extname(file);\n\n  switch (ext) {\n    case '.js':\n      return { result: importDefault(file) };\n    case '.json':\n      return { result: await fse.readJSON(file) };\n    default:\n      return { result: {} };\n  }\n};\n"],"names":[],"mappings":"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"}