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:

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.cr

Constant 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. 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 Method Summary

Instance Method Summary

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

def self.tech_name : String #

[View source]

Instance Method Detail

def analyze #

[View source]
def tech : String #

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.


[View source]