class
Analyzer::Javascript::Feathers
Overview
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(), ...) withoutnew.- an inline object literal
{ ... }. - a bare identifier that resolves (same-file or one
require/importhop, viaNoir::ImportGraph) to a class or object literal — but NOT toexpress.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 Expressapp.
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.
Defined in:
analyzer/analyzers/javascript/feathers.crConstant Summary
-
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.
mmakes^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 Method Summary
Instance Method Summary
- #analyze
-
#tech : String
Instance-side view of the same declaration.
Instance methods inherited from class Analyzer
analyze
analyze,
base_path : String
base_path,
base_paths : Array(String)
base_paths,
base_relative_path(path : String) : String
base_relative_path,
callees_needed? : Bool
callees_needed?,
content_matches?(content : String, markers : Regex) : Bool
content_matches?,
http_header_name(name : String) : String | Nil
http_header_name,
line_number_for_index(content : String, char_index : Int32) : Int32
line_number_for_index,
logger : NoirLogger
logger,
parallel_analyze(files : Array(String), &block : String -> Nil)
parallel_analyze,
read_file_content(path : String) : String
read_file_content,
result : Array(Endpoint)
result,
tech : String
tech,
unique_params(params : Array(Param)) : Array(Param)
unique_params,
url : String
url,
web_root_path(path : String, markers : Array(String)) : String
web_root_path
Constructor methods inherited from class Analyzer
new(options : Hash(String, YAML::Any))
new
Macros inherited from class Analyzer
analyzer_for(tech)
analyzer_for
Instance methods inherited from module FileHelper
all_files : Array(String)
all_files,
get_files_by_basename(basename : String) : Array(String)
get_files_by_basename,
get_files_by_extension(extension : String) : Array(String)
get_files_by_extension,
get_files_by_extensions(extensions : Array(String)) : Array(String)
get_files_by_extensions,
get_files_by_prefix(prefix : String) : Array(String)
get_files_by_prefix,
get_files_by_prefix_and_extension(prefix : String, extension : String) : Array(String)
get_files_by_prefix_and_extension,
get_files_by_relative_path(relative_path : String, root : String = "") : Array(String)
get_files_by_relative_path,
get_public_dir_files(base_path : String, folder : String) : Array(String)
get_public_dir_files,
get_public_files(base_path : String, anchors : Array(String) = ["shard.yml", "Gemfile"]) : Array(String)
get_public_files,
walked_path(expanded : String) : String
walked_path
Class Method Detail
Instance Method Detail
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.