@decaf-ts/core
Version:
Core persistence module for the decaf framework
698 lines • 30.7 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Adapter = void 0;
const db_decorators_1 = require("@decaf-ts/db-decorators");
const decorator_validation_1 = require("@decaf-ts/decorator-validation");
const constants_1 = require("./constants.cjs");
const logging_1 = require("@decaf-ts/logging");
const ObserverHandler_1 = require("./ObserverHandler.cjs");
const Context_1 = require("./Context.cjs");
const decoration_1 = require("@decaf-ts/decoration");
const errors_1 = require("./errors.cjs");
const ContextualLoggedClass_1 = require("./../utils/ContextualLoggedClass.cjs");
const utils_1 = require("./../utils/utils.cjs");
const flavourResolver = decoration_1.Decoration["flavourResolver"].bind(decoration_1.Decoration);
decoration_1.Decoration["flavourResolver"] = (obj) => {
try {
const result = flavourResolver(obj);
if (result && result !== decoration_1.DefaultFlavour)
return result;
const targetCtor = typeof obj === "function"
? obj
: obj?.constructor;
const registeredFlavour = targetCtor && typeof decoration_1.Metadata["registeredFlavour"] === "function"
? decoration_1.Metadata.registeredFlavour(targetCtor)
: undefined;
if (registeredFlavour && registeredFlavour !== decoration_1.DefaultFlavour)
return registeredFlavour;
const currentFlavour = Adapter["_currentFlavour"];
if (currentFlavour) {
const cachedAdapter = Adapter["_cache"]?.[currentFlavour];
if (cachedAdapter?.flavour)
return cachedAdapter.flavour;
return currentFlavour;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
return decoration_1.DefaultFlavour;
}
};
/**
* @description Abstract Facade class for persistence adapters
* @summary Provides the foundation for all database adapters in the persistence layer. This class
* implements several interfaces to provide a consistent API for database operations, observer
* pattern support, and error handling. It manages adapter registration, CRUD operations, and
* observer notifications.
* @template CONFIG - The underlying persistence driver config
* @template QUERY - The query object type used by the adapter
* @template FLAGS - The repository flags type
* @template CONTEXT - The context type
* @param {CONFIG} _config - The underlying persistence driver config
* @param {string} flavour - The identifier for this adapter type
* @param {string} [_alias] - Optional alternative name for this adapter
* @class Adapter
* @example
* ```typescript
* // Implementing a concrete adapter
* class PostgresAdapter extends Adapter<pg.PoolConfig, pg.Query, PostgresFlags, PostgresContext> {
* constructor(client: pg.PoolConfig) {
* super(client, 'postgres');
* }
*
* async initialize() {
* // Set up the adapter
* await this.native.connect();
* }
*
* async create(tableName, id, model) {
* // Implementation for creating records
* const columns = Object.keys(model).join(', ');
* const values = Object.values(model);
* const placeholders = values.map((_, i) => `$${i+1}`).join(', ');
*
* const query = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
* const result = await this.native.query(query, values);
* return result.rows[0];
* }
*
* // Other required method implementations...
* }
*
* // Using the adapter
* const pgClient = new pg.Client(connectionString);
* const adapter = new PostgresAdapter(pgClient);
* await adapter.initialize();
*
* // Set as the default adapter
* Adapter.setCurrent('postgres');
*
* // Perform operations
* const user = await adapter.create('users', 1, { name: 'John', email: 'john@example.com' });
* ```
* @mermaid
* classDiagram
* class Adapter {
* +Y native
* +string flavour
* +string alias
* +create(tableName, id, model)
* +read(tableName, id)
* +update(tableName, id, model)
* +delete(tableName, id)
* +observe(observer, filter)
* +unObserve(observer)
* +static current
* +static get(flavour)
* +static setCurrent(flavour)
* }
*
* class RawExecutor {
* +raw(query)
* }
*
* class Observable {
* +observe(observer, filter)
* +unObserve(observer)
* +updateObservers(table, event, id)
* }
*
* class Observer {
* +refresh(table, event, id)
* }
*
* class ErrorParser {
* +parseError(err)
* }
*
* Adapter --|> RawExecutor
* Adapter --|> Observable
* Adapter --|> Observer
* Adapter --|> ErrorParser
*/
class Adapter extends ContextualLoggedClass_1.ContextualLoggedClass {
static { this._cache = {}; }
/**
* @description Gets the native persistence config
* @summary Provides access to the underlying persistence driver config
* @template CONF
* @return {CONF} The native persistence driver config
*/
get config() {
return this._config;
}
/**
* @description Gets the adapter's alias or flavor name
* @summary Returns the alias if set, otherwise returns the flavor name
* @return {string} The adapter's identifier
*/
get alias() {
return this._alias || this.flavour;
}
/**
* @description Gets the repository constructor for this adapter
* @summary Returns the constructor for creating repositories that work with this adapter
* @template M - The model type
* @return {Constructor<Repository<any, Adapter<CONF, CONN, QUERY, CONTEXT>>>} The repository constructor
*/
repository() {
if (!Adapter._baseRepository)
throw new db_decorators_1.InternalError(`This should be overridden when necessary. Otherwise it will be replaced lazily`);
return Adapter._baseRepository;
}
async shutdownProxies(k) {
if (!this.proxies)
return;
if (k && !(k in this.proxies))
throw new db_decorators_1.InternalError(`No proxy found for ${k}`);
if (!k) {
for (const key in this.proxies) {
try {
await this.proxies[key].shutdown();
}
catch (e) {
this.log.error(`Failed to shutdown proxied adapter ${key}: ${e}`);
continue;
}
delete this.proxies[key];
}
}
else {
try {
await this.proxies[k].shutdown();
delete this.proxies[k];
}
catch (e) {
this.log.error(`Failed to shutdown proxied adapter ${k}: ${e}`);
}
}
}
/**
* @description Shuts down the adapter
* @summary Performs any necessary cleanup tasks, such as closing connections
* When overriding this method, ensure to call the base method first
* @return {Promise<void>} A promise that resolves when shutdown is complete
*/
async shutdown() {
await this.shutdownProxies();
if (this.dispatch)
await this.dispatch.close();
}
/**
* @description Creates a new adapter instance
* @summary Initializes the adapter with the native driver and registers it in the adapter cache
*/
constructor(_config, flavour, _alias) {
super();
this._config = _config;
this.flavour = flavour;
this._alias = _alias;
/**
* @description The context constructor for this adapter
* @summary Reference to the context class constructor used by this adapter
*/
this.Context = (Context_1.Context);
if (this.alias in Adapter._cache)
throw new db_decorators_1.InternalError(`${this.alias} persistence adapter ${this._alias ? `(${this.flavour}) ` : ""} already registered`);
Adapter._cache[this.alias] = this;
this.log.info(`Created ${this.alias} persistence adapter ${this._alias ? `(${this.flavour}) ` : ""} persistence adapter`);
if (!Adapter._currentFlavour) {
this.log.verbose(`Defined ${this.alias} persistence adapter as current`);
Adapter._currentFlavour = this.alias;
}
}
/**
* @description Creates a new dispatch instance
* @summary Factory method that creates a dispatch instance for this adapter
* @return {Dispatch} A new dispatch instance
*/
Dispatch() {
return new Adapter._baseDispatch();
}
/**
* @description Creates a new observer handler
* @summary Factory method that creates an observer handler for this adapter
* @return {ObserverHandler} A new observer handler instance
*/
ObserverHandler() {
return new ObserverHandler_1.ObserverHandler();
}
/**
* @description Checks if an attribute name is reserved
* @summary Determines if a given attribute name is reserved and cannot be used as a column name
* @param {string} attr - The attribute name to check
* @return {boolean} True if the attribute is reserved, false otherwise
*/
isReserved(attr) {
return !attr;
}
/**
* @description Initializes the adapter
* @summary Performs any necessary setup for the adapter, such as establishing connections
* @param {...any[]} args - Initialization arguments
* @return {Promise<void>} A promise that resolves when initialization is complete
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async initialize(...args) { }
/**
* @description Creates a sequence generator
* @summary Factory method that creates a sequence generator for generating sequential values
* @param {SequenceOptions} options - Configuration options for the sequence
* @return {Promise<Sequence>} A promise that resolves to a new sequence instance
*/
async Sequence(options) {
return new Adapter._baseSequence(options, this);
}
/**
* @description Creates repository flags for an operation
* @summary Generates a set of flags that describe a database operation, combining default flags with overrides
* @template F - The Repository Flags type
* @template M - The model type
* @param {OperationKeys} operation - The type of operation being performed
* @param {Constructor<M>} model - The model constructor
* @param {Partial<F>} flags - Custom flag overrides
* @param {...any[]} args - Additional arguments
* @return {Promise<F>} The complete set of flags
*/
async flags(operation, model, flags,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
...args) {
let log = (flags.logger || logging_1.Logging.for(this.toString()));
if (flags.correlationId)
log = log.for({ correlationId: flags.correlationId });
return Object.assign({}, constants_1.DefaultAdapterFlags, flags, {
affectedTables: (Array.isArray(model) ? model : [model]).map(decorator_validation_1.Model.tableName),
writeOperation: operation !== db_decorators_1.OperationKeys.READ,
timestamp: new Date(),
operation: operation,
ignoredValidationProperties: decoration_1.Metadata.validationExceptions(Array.isArray(model) && model[0]
? model[0]
: model, operation),
logger: log,
});
}
/**
* @description Creates a context for a database operation
* @summary Generates a context object that describes a database operation, used for tracking and auditing
* @template F - The Repository flags type
* @template M - The model type
* @param {OperationKeys.CREATE|OperationKeys.READ|OperationKeys.UPDATE|OperationKeys.DELETE} operation - The type of operation
* @param {Partial<F>} overrides - Custom flag overrides
* @param {Constructor<M>} model - The model constructor
* @param {...any[]} args - Additional arguments
* @return {Promise<C>} A promise that resolves to the context object
*/
async context(operation, overrides, model, ...args) {
const log = this.log.for(this.context);
log.debug(`Creating new context for ${operation} operation on ${model ? (Array.isArray(model) ? model.map((m) => m.name) : model.name) : "no"} model with flag overrides: ${JSON.stringify(overrides)}`);
const flags = await this.flags(operation, model, overrides, ...args);
return new this.Context().accumulate(flags);
}
/**
* @description Prepares a model for persistence
* @summary Converts a model instance into a format suitable for database storage,
* handling column mapping and separating transient properties
* handling column mapping and separating transient properties
* @template M - The model type
* @param {M} model - The model instance to prepare
* @param args - optional args for subclassing purposes
* @return The prepared data
*/
prepare(model, ...args) {
const { log } = this.logCtx(args, this.prepare);
const split = model.segregate();
const result = Object.entries(split.model).reduce((accum, [key, val]) => {
if (typeof val === "undefined")
return accum;
const mappedProp = decorator_validation_1.Model.columnName(model.constructor, key);
if (this.isReserved(mappedProp))
throw new db_decorators_1.InternalError(`Property name ${mappedProp} is reserved`);
accum[mappedProp] = val;
return accum;
}, {});
if (model[constants_1.PersistenceKeys.METADATA]) {
// TODO movo to couchdb
log.silly(`Passing along persistence metadata for ${model[constants_1.PersistenceKeys.METADATA]}`);
Object.defineProperty(result, constants_1.PersistenceKeys.METADATA, {
enumerable: false,
writable: true,
configurable: true,
value: model[constants_1.PersistenceKeys.METADATA],
});
}
return {
record: result,
id: model[decorator_validation_1.Model.pk(model.constructor)],
transient: split.transient,
};
}
/**
* @description Converts database data back into a model instance
* @summary Reconstructs a model instance from database data, handling column mapping
* and reattaching transient properties
* @template M - The model type
* @param obj - The database record
* @param {Constructor<M>} clazz - The model class or name
* @param pk - The primary key property name
* @param {string|number|bigint} id - The primary key value
* @param [transient] - Transient properties to reattach
* @param [args] - options args for subclassing purposes
* @return {M} The reconstructed model instance
*/
revert(obj, clazz, id, transient, ...args) {
const { log, ctx } = this.logCtx(args, this.revert);
const ob = {};
const pk = decorator_validation_1.Model.pk(clazz);
ob[pk] = id;
const m = new clazz(ob);
log.silly(`Rebuilding model ${m.constructor.name} id ${id}`);
const metadata = obj[constants_1.PersistenceKeys.METADATA]; // TODO move to couchdb
const result = Object.keys(m).reduce((accum, key) => {
accum[key] =
obj[decorator_validation_1.Model.columnName(clazz, key)];
return accum;
}, m);
if (ctx.get("rebuildWithTransient") && transient) {
log.verbose(`re-adding transient properties: ${Object.keys(transient).join(", ")}`);
Object.entries(transient).forEach(([key, val]) => {
if (key in result)
throw new db_decorators_1.InternalError(`Transient property ${key} already exists on model ${m.constructor.name}. should be impossible`);
result[key] = val;
});
}
if (metadata) {
// TODO move to couchdb
log.silly(`Passing along ${this.flavour} persistence metadata for ${m.constructor.name} id ${id}: ${metadata}`);
Object.defineProperty(result, constants_1.PersistenceKeys.METADATA, {
enumerable: false,
configurable: true,
writable: true,
value: metadata,
});
}
return result;
}
/**
* @description Creates multiple records in the database
* @summary Inserts multiple records with the given IDs and data into the specified table
* @param {string} tableName - The name of the table to insert into
* @param id - The identifiers for the new records
* @param model - The data to insert for each record
* @param {...any[]} args - Additional arguments specific to the adapter implementation
* @return A promise that resolves to an array of created records
*/
async createAll(clazz, id, model, ...args) {
if (id.length !== model.length)
throw new db_decorators_1.InternalError("Ids and models must have the same length");
const { log, ctxArgs } = this.logCtx(args, this.createAll);
const tableLabel = decorator_validation_1.Model.tableName(clazz);
log.debug(`Creating ${id.length} entries ${tableLabel} table`);
return (0, utils_1.promiseSequence)(id.map((i, count) => () => this.create(clazz, i, model[count], ...ctxArgs)));
}
/**
* @description Retrieves multiple records from the database
* @summary Fetches multiple records with the given IDs from the specified table
* @param {string} tableName - The name of the table to read from
* @param id - The identifiers of the records to retrieve
* @param {...any[]} args - Additional arguments specific to the adapter implementation
* @return A promise that resolves to an array of retrieved records
*/
async readAll(clazz, id, ...args) {
const { log, ctxArgs } = this.logCtx(args, this.readAll);
const tableName = decorator_validation_1.Model.tableName(clazz);
log.debug(`Reading ${id.length} entries ${tableName} table`);
return (0, utils_1.promiseSequence)(id.map((i) => () => this.read(clazz, i, ...ctxArgs)));
}
/**
* @description Updates multiple records in the database
* @summary Modifies multiple existing records with the given IDs in the specified table
* @param {Constructor<M>} tableName - The name of the table to update
* @param {string[]|number[]} id - The identifiers of the records to update
* @param model - The new data for each record
* @param {...any[]} args - Additional arguments specific to the adapter implementation
* @return A promise that resolves to an array of updated records
*/
async updateAll(clazz, id, model, ...args) {
if (id.length !== model.length)
throw new db_decorators_1.InternalError("Ids and models must have the same length");
const { log, ctxArgs } = this.logCtx(args, this.updateAll);
const tableLabel = decorator_validation_1.Model.tableName(clazz);
log.debug(`Updating ${id.length} entries ${tableLabel} table`);
return (0, utils_1.promiseSequence)(id.map((i, count) => () => this.update(clazz, i, model[count], ...ctxArgs)));
}
/**
* @description Deletes multiple records from the database
* @summary Removes multiple records with the given IDs from the specified table
* @param {string} tableName - The name of the table to delete from
* @param id - The identifiers of the records to delete
* @param {...any[]} args - Additional arguments specific to the adapter implementation
* @return A promise that resolves to an array of deleted records
*/
async deleteAll(tableName, id, ...args) {
const { log, ctxArgs } = Adapter.logCtx(args, this.deleteAll);
log.debug(`Deleting ${id.length} entries from ${tableName} table`);
return (0, utils_1.promiseSequence)(id.map((i) => () => this.delete(tableName, i, ...ctxArgs)));
}
/**
* @description Registers an observer for database events
* @summary Adds an observer to be notified about database changes. The observer can optionally
* provide a filter function to receive only specific events.
* @param {Observer} observer - The observer to register
* @param {ObserverFilter} [filter] - Optional filter function to determine which events the observer receives
* @return {void}
*/
observe(observer, filter) {
if (!this.observerHandler)
Object.defineProperty(this, "observerHandler", {
value: this.ObserverHandler(),
writable: false,
});
this.observerHandler.observe(observer, filter);
const log = this.log.for(this.observe);
log.verbose(`Registering new observer ${observer.toString()}`);
if (!this.dispatch) {
log.info(`Creating dispatch for ${this.alias}`);
this.dispatch = this.Dispatch();
this.dispatch.observe(this);
}
}
/**
* @description Unregisters an observer
* @summary Removes a previously registered observer so it no longer receives database event notifications
* @param {Observer} observer - The observer to unregister
* @return {void}
*/
unObserve(observer) {
if (!this.observerHandler)
throw new db_decorators_1.InternalError("ObserverHandler not initialized. Did you register any observables?");
this.observerHandler.unObserve(observer);
this.log
.for(this.unObserve)
.verbose(`Observer ${observer.toString()} removed`);
}
/**
* @description Notifies all observers about a database event
* @summary Sends notifications to all registered observers about a change in the database,
* filtering based on each observer's filter function
* @param {string} table - The name of the table where the change occurred
* @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of operation that occurred
* @param {EventIds} id - The identifier(s) of the affected record(s)
* @param {...any[]} args - Additional arguments to pass to the observers
* @return {Promise<void>} A promise that resolves when all observers have been notified
*/
async updateObservers(table, event, id, ...args) {
if (!this.observerHandler)
throw new db_decorators_1.InternalError("ObserverHandler not initialized. Did you register any observables?");
const { log, ctxArgs } = Adapter.logCtx(args, this.updateObservers);
log.verbose(`Updating ${this.observerHandler.count()} observers for adapter ${this.alias}: Event: `);
await this.observerHandler.updateObservers(table, event, id, ...ctxArgs);
}
/**
* @description Refreshes data based on a database event
* @summary Implementation of the Observer interface method that delegates to updateObservers
* @param {string} table - The name of the table where the change occurred
* @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of operation that occurred
* @param {EventIds} id - The identifier(s) of the affected record(s)
* @param {...any[]} args - Additional arguments related to the event
* @return {Promise<void>} A promise that resolves when the refresh is complete
*/
async refresh(table, event, id, ...args) {
return this.updateObservers(table, event, id, ...args);
}
/**
* @description Gets a string representation of the adapter
* @summary Returns a human-readable string identifying this adapter
* @return {string} A string representation of the adapter
*/
toString() {
return `${this.flavour} adapter`;
}
/**
* @description Gets the adapter flavor associated with a model
* @summary Retrieves the adapter flavor that should be used for a specific model class
* @template M - The model type
* @param {Constructor<M>} model - The model constructor
* @return {string} The adapter flavor name
*/
static flavourOf(model) {
return decoration_1.Metadata.flavourOf(model);
}
static get currentFlavour() {
if (!Adapter._currentFlavour)
throw new db_decorators_1.InternalError(`No persistence flavour set. Please initialize your adapter`);
return Adapter._currentFlavour;
}
/**
* @description Gets the current default adapter
* @summary Retrieves the adapter that is currently set as the default for operations
* @return {Adapter<any, any, any, any>} The current adapter
*/
static get current() {
return Adapter.get(this.currentFlavour);
}
/**
* @description Gets an adapter by flavor
* @summary Retrieves a registered adapter by its flavor name
* @template CONF - The database driver config
* @template CONN - The database driver instance
* @template QUERY - The query type
* @template CONTEXT - The context type
* @param {string} flavour - The flavor name of the adapter to retrieve
* @return {Adapter<CONF, CONN, QUERY, CONTEXT> | undefined} The adapter instance or undefined if not found
*/
static get(flavour) {
if (!flavour)
return Adapter.get(this._currentFlavour);
if (flavour in this._cache)
return this._cache[flavour];
throw new db_decorators_1.InternalError(`No Adapter registered under ${flavour}.`);
}
/**
* @description Sets the current default adapter
* @summary Changes which adapter is used as the default for operations
* @param {string} flavour - The flavor name of the adapter to set as current
* @return {void}
*/
static setCurrent(flavour) {
this._currentFlavour = flavour;
}
/**
* @description Gets all models associated with an adapter flavor
* @summary Retrieves all model constructors that are configured to use a specific adapter flavor
* @template M - The model type
* @param {string} flavour - The adapter flavor to find models for
* @return An array of model constructors
*/
static models(flavour) {
try {
return decoration_1.Metadata.flavouredAs(flavour).filter(decorator_validation_1.Model.isModel);
}
catch (e) {
throw new db_decorators_1.InternalError(e);
}
}
static decoration() { }
static logCtx(args, method) {
return super.logCtx(args, method);
}
get client() {
if (!this._client) {
this._client = this.getClient();
}
return this._client;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for(config, ...args) {
if (!this.proxies)
this.proxies = {};
const key = `${this.alias} - ${(0, decorator_validation_1.hashObj)(config)}`;
if (key in this.proxies)
return this.proxies[key];
let client;
const proxy = new Proxy(this, {
get: (target, p, receiver) => {
if (p === "_config") {
const originalConf = Reflect.get(target, p, receiver);
return Object.assign({}, originalConf, config);
}
if (p === "_client") {
return client;
}
return Reflect.get(target, p, receiver);
},
set: (target, p, value, receiver) => {
if (p === "_client") {
client = value;
return true;
}
return Reflect.set(target, p, value, receiver);
},
});
this.proxies[key] = proxy;
return proxy;
}
migrations() {
return decoration_1.Metadata.migrationsFor(this);
}
async getQueryRunner() {
return this;
}
async migrate(migrations = this.migrations(), ...args) {
if (migrations instanceof Context_1.Context) {
args = [migrations];
migrations = this.migrations();
}
const { ctx } = Adapter.logCtx(args, this.migrate);
const qr = await this.getQueryRunner();
for (const migration of migrations) {
try {
const m = new migration();
await m.up(qr, this, ctx);
await m.down(qr, this, ctx);
}
catch (e) {
throw new errors_1.MigrationError(e);
}
}
}
}
exports.Adapter = Adapter;
__decorate([
(0, logging_1.final)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], Adapter.prototype, "shutdownProxies", null);
__decorate([
(0, logging_1.final)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object, Object, Object]),
__metadata("design:returntype", Promise)
], Adapter.prototype, "context", null);
__decorate([
(0, logging_1.final)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Function]),
__metadata("design:returntype", void 0)
], Adapter.prototype, "observe", null);
__decorate([
(0, logging_1.final)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], Adapter.prototype, "unObserve", null);
__decorate([
(0, logging_1.final)(),
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], Adapter.prototype, "client", null);
//# sourceMappingURL=Adapter.js.map