Extending Featurevisor
Datasource and adapters
Featurevisor reads and writes definitions through a datasource. By default it uses the files in your repository, and an adapter can replace them with another store.
Plugins receive the datasource as datasource. Its methods work the same whatever the format or the adapter. The source has the full signatures.
Definitions#
Every kind of definition has five methods. For features:
const keys = await datasource.listFeatures()const exists = await datasource.featureExists('checkout')const checkout = await datasource.readFeature('checkout')await datasource.writeFeature('checkout', checkout)await datasource.deleteFeature('checkout')The other kinds use their own names in the same places:
| Kind | Methods |
|---|---|
| Features | listFeatures, featureExists, readFeature, writeFeature, deleteFeature |
| Global variables | listVariables, variableExists, readVariable, writeVariable, deleteVariable |
| Segments | listSegments, segmentExists, readSegment, writeSegment, deleteSegment |
| Attributes | listAttributes, attributeExists, readAttribute, writeAttribute, deleteAttribute |
| Schemas | listSchemas, schemaExists, readSchema, writeSchema, deleteSchema |
| Targets | listTargets, targetExists, readTarget, writeTarget, deleteTarget |
| Tests | listTests, readTest, writeTest, deleteTest |
| Scenarios | listScenarios, scenarioExists, readScenario, writeScenario, deleteScenario |
Global variable methods work on files in variables/, not on variables inside a feature.
Dependencies#
getRequiredFeaturesChain() returns a Set of the feature and every feature its conditions reference, however indirectly:
await datasource.getRequiredFeaturesChain('chatFileUpload')// Set { 'chatFileUpload', 'liveChat', 'pricing' }getRequiredFeaturesChainForVariable() returns the features a global variable depends on, through its root conditions, its overrides, any composition sources and the other global variables those read. It throws if the variable does not exist:
await datasource.getRequiredFeaturesChainForVariable( 'supportEmail',)// Set { 'checkout' }Nested attributes#
listFlattenedAttributes() lists attributes with the properties of object attributes in dot form:
await datasource.listFlattenedAttributes()// ['account', 'account.locale', 'account.seats', 'city', ...]Test and scenario files#
Test and scenario keys come from their paths under tests/, without .spec, such as features.checkout. Use the keys listTests() and listScenarios() return.
getTestSourcePath(key)returns the path of a test's file, including an existing.spec.ymlname.getScenarioSourcePath(key)does the same for a scenario, and throws if the adapter cannot give one.getTestSpecName(key)builds a file name such asfeatures.checkout.ymlfrom the key and the parser's extension, without looking for the file.
All three return a string, without a promise.
Revision and state#
const revision = await datasource.readRevision()await datasource.writeRevision(String(Number(revision) + 1))The revision is a string. Convert it to a number to increase it.
const state = await datasource.readState('production')await datasource.writeState('production', state)Pass false instead of an environment name in a project without environments.
Datafiles#
listDatafiles() lists built datafiles, each with its path relative to the datafiles directory and its size and gzipSize in bytes. A datafile is read and written by its environment, or false, and its target:
const datafile = await datasource.readDatafile({ environment: false, target: 'web',})await datasource.writeDatafile(datafile, { environment: false, target: 'web',})datafilesDir in the options overrides datafilesDirectoryPath for that call.
History#
listHistoryEntries() lists the Git history of one definition, or of the whole project when called without arguments:
const entries = await datasource.listHistoryEntries( 'feature', 'checkout',)The type is one of feature, segment, attribute, schema, variable, target, test or scenario. readCommit() reads one commit, optionally narrowed to one definition:
const commit = await datasource.readCommit(hash)const change = await datasource.readCommit(hash, 'feature', 'checkout')Sets#
In a project with sets, a datasource works on one set at a time. One created without a set reads and writes the project root.
const sets = await datasource.listSets()const current = datasource.getSet()const storefront = datasource.forSet('storefront')const keys = await storefront.listFeatures()forSet() returns a new datasource with that set's directories. A plugin that supports sets should honour --set and choose the set it names, or it reads the wrong tree.
getConfig() returns the resolved configuration for the datasource, set included, and getExtension() returns the parser's extension, such as yml.
Adapters#
The default adapter, FilesystemAdapter, keeps definitions as files in the repository. Write your own only to store them somewhere else.
Write an adapter#
Extend Adapter from @featurevisor/core, and implement every method below:
import { Adapter } from '@featurevisor/core'export class CustomAdapter extends Adapter { // implement every method in the table}| Method | Returns | Does |
|---|---|---|
listSets() | Promise<string[]> | Lists the sets |
listEntities(entityType) | Promise<string[]> | Lists the keys of one kind |
entityExists(entityType, entityKey) | Promise<boolean> | Checks that a definition exists |
readEntity<T>(entityType, entityKey) | Promise<T> | Reads a parsed definition |
writeEntity<T>(entityType, entityKey, entity) | Promise<T> | Stores a definition and returns it |
deleteEntity(entityType, entityKey) | Promise<void> | Deletes a definition |
readState(environment) | Promise<ExistingState> | Reads build state |
writeState(environment, existingState) | Promise<void> | Stores build state |
readDatafile(options) | Promise<DatafileContent> | Reads a built datafile |
writeDatafile(datafileContent, options) | Promise<void> | Stores a built datafile |
readRevision() | Promise<string> | Reads the revision |
writeRevision(revision) | Promise<void> | Stores the revision |
listHistoryEntries(entityType?, entityKey?) | Promise<HistoryEntry[]> | Lists project or definition history |
readCommit(commitHash, entityType?, entityKey?) | Promise<Commit> | Reads a commit, optionally for one definition |
entityType takes the types listed under History. State methods take an environment name or false, and datafile methods take the options shown under Datafiles.
These are optional:
listDatafiles(). Without it, the datasource returns an empty list.getTestSourcePath(testKey)andgetEntityPath(entityType, entityKey). Without them, test paths are built from the configuration.getScenarioSourcePath(scenarioKey). There is no fallback for scenarios.
The datasource creates your adapter with the resolved configuration and, optionally, the project root. Use them to follow each set's paths. Adapter has the exact types, and FilesystemAdapter is a complete example.
Use an adapter#
Set it in the configuration:
const { CustomAdapter } = require('./adapters/custom-adapter')module.exports = { adapter: CustomAdapter,}
