class

Analyzer::Javascript::Feathers

Inherits Analyzer::Javascript::JavascriptEngine < Analyzer < FileHelper < Reference < Object

Feathers.js (https://feathersjs.com) is service-based rather than route-based: registering a service at a path auto-generates REST verbs from whichever of the standard CRUD methods the service implements.

app.use('/messages', new MessageService())
app.service('messages').hooks({ ... })

find(params) -> GET /messages get(id, params) -> GET /messages/:id create(data, params) -> POST /messages update(id, data, params) -> PUT /messages/:id patch(id, data, params) -> PATCH /messages/:id remove(id, params) -> DELETE /messages/:id

app.use(path, service, options) accepts an optional third argument whose methods: array is the authoritative list of externally exposed methods when present (v5 "Dove" API) — it is honoured here as an intersection against whatever methods were otherwise detected/assumed. The path and methods: value are both frequently bare identifiers pointing at a sibling <name>.shared.ts module in the current (v5/"Dove") CLI generator layout (export const messagePath = 'messages', export const messageMethods = ['find', 'get', ...] as const) rather than inline literals — both are resolved the same one-import-hop way as the service class itself.

False-positive risk

app.use('/path', someExpression) is also the generic Express middleware/router-mount idiom, and Express coexists with Feathers in the same JS/Node ecosystem noir already supports. To avoid stealing routes from a plain Express app (or a sibling Express app in a monorepo), a .use() call is only treated as a Feathers service registration when the second argument is structurally service-shaped:

  • new SomeClass(...) — Express never mounts a freshly-constructed instance this way; routers/middleware are always factory calls (express.Router(), cors(), ...) without new.
  • an inline object literal { ... }.
  • a bare identifier that resolves (same-file or one require/ import hop, via Noir::ImportGraph) to a class or object literal — but NOT to express.Router()/Router().
  • a bare identifier that cannot be resolved at all, but the same file also calls app.service(<same path>) — an API Express apps never have, since .service() doesn't exist on a plain Express app.

A identifier(...) / member.expr(...) call (the shape of express.Router(), cors(), express.static(...), ...) is never accepted, so ordinary Express middleware mounting is left alone.

When the service expression resolves to a real class/object body, only the CRUD methods that body actually defines are emitted — a service that only implements find/get does not get create/ update/patch/remove fabricated for it. The one exception is a class that extends one of the well-known Feathers database adapters (KnexService, MongoDBService, MemoryService, ...) — those always implement the full CRUD set themselves regardless of which methods the subclass overrides, which is how the official CLI generator's default <name>.class.ts looks (export class MessageService extends KnexService<...> {}, no method bodies at all).

When the expression can't be resolved to a body at all (external package, dynamic value, ...) but the structural evidence above is still strong enough to be confident this IS a Feathers registration, the full 6-verb CRUD set is emitted as a documented, deliberately conservative fallback — never for a body we DID inspect, found to extend nothing adapter-like, and found zero CRUD-shaped methods in, which is treated as a real negative (nothing emitted) rather than a fallback trigger.

Constants

BODY_METHODS = Set {"create", "update", "patch"}
CRUD_METHODS = ["find", "get", "create", "update", "patch", "remove"] of ::String
CRUD_VERB = {"find" => {"GET", false}, "get" => {"GET", true}, "create" => {"POST", false}, "update" => {"PUT", true}, "patch" => {"PATCH", true}, "remove" => {"DELETE", true}}

method name => {HTTP verb, needs a trailing /:id segment}

HEADER_PARAM_RE = /\bparams\.headers\.(\w+)|\bparams\.headers\[\s*['"]([\w-]+)['"]\s*\]/
JS_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx"]
KNOWN_ADAPTER_BASE_CLASSES = Set {"Service", "AdapterService", "KnexService", "MongoDBService", "MemoryService", "SequelizeService", "NeDBService", "MikroOrmService", "ObjectionService", "PrismaService", "RethinkDBService", "MongooseService", "FeathersSequelize"}

The officially documented Feathers database-adapter service base classes (https://feathersjs.com/api/databases/adapters) — every one of these implements the full CRUD set itself, so a subclass that overrides none (or only some) of them still exposes all six externally, unless narrowed by an explicit methods: option.

METHOD_COLON_RE = CRUD_METHODS.to_h do |m| {m, /^[ \t]*#{m}\s*:/m} end
METHOD_PAREN_RE = CRUD_METHODS.to_h do |m| {m, /^[ \t]*(?:public\s+|private\s+|protected\s+|static\s+|async\s+)*#{m}\s*\(/m} end

Crystal recompiles an interpolated regex literal on every evaluation; these are keyed by the (small, fixed) CRUD method name set, so build them once at load time rather than per call. m makes ^ match at each line start within a multi-line body, not just the start of the whole string.

QUERY_DESTRUCTURE_RE = /(?:const|let|var)\s*\{\s*([^}]+)\}\s*=\s*params\.query\b/
QUERY_PARAM_RE = /\bparams\.query\.(\w+)|\bparams\.query\[\s*['"](\w+)['"]\s*\]/
SOLE_CLASS_RE = /\bclass\s+([A-Za-z_$][\w$]*)\b[^{]*\{/
USE_CALL_RE = /\.use\s*\(/

Class methods

tech_name
Source

Instance methods

analyze
Source
tech

Instance-side view of the same declaration. The per-file rescues live on this base class, which has no way to name the analyzer that is running inside them, so a skipped file could not be attributed to a tech. Deriving it from analyzer_for keeps the name written exactly once.

Source