Skip to content

Commit

Permalink
feat: add option to locally disable caching
Browse files Browse the repository at this point in the history
  • Loading branch information
hasezoey committed Feb 9, 2023
1 parent 951cb54 commit 78ac3bc
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 119 deletions.
49 changes: 49 additions & 0 deletions docs/api/decorators/modelOptions.md
Expand Up @@ -266,3 +266,52 @@ await area.save();
```
See [Nested Discriminators](../../guides/advanced/nested-discriminators.mdx) for a guide on how to use nested Discriminators.
### disableCaching
Default: `false`
Disable Caching for current Class (and all classes extending it) or for just a call (for [`buildSchema`](../functions/buildSchema.md) / [`getModelForClass`](../functions/getModelForClass.md) / [`getDiscriminatorModelForClass`](../functions/getDiscriminatorModelForClass.md)).
This Option will NOT overwrite the global [`disableCaching`](../functions/setGlobalOptions.md#disablecaching).
Example:
```ts
// some values to keep references
let KittenModel1: mongoose.Model<any>;
let KittenModel2: mongoose.Model<any>;
let KittenClass1: AnyParamConstructor<any>;
let KittenClass2: AnyParamConstructor<any>;
{
class Kitten {
@prop()
public name?: string;
}

KittenModel1 = getModelForClass(Kitten, { options: { disableCaching: true } });
KittenClass1 = Kitten;
}
assert.ok(getModelWithString(getName(KittenClass1)) === undefined); // caching was disabled locally, so it cannot be found - because it was never added
{
class Kitten {
@prop()
public nameTag?: string;
}

KittenModel2 = getModelForClass(Kitten, {
existingConnection: mongoose.createConnection(),
});
KittenClass2 = Kitten;
}
assert.ok(getModelWithString(getName(KittenClass2))); // caching was enabled, so the second can be found

// the following will return the "KittenModel2" instance, because both classes have the same name but only the second one was added to the caching
// and caching currently works by (typegoose generated) name
const KittenModel3 = getModelForClass(KittenClass1);
// Note that the above *would* work if "disableCaching" would be defined via a "@modelOptions" decorator, because then caching would also have been disabled here

assert.ok(KittenModel1 !== KittenModel2); // check that both original models do not match, because caching was disabled they are different

assert.ok(KittenModel3 === KittenModel2); // check that "KittenModel3" is the same reference as "KittenModel2", because "KittenClass2" was added with caching and has the same name
```
6 changes: 4 additions & 2 deletions docs/api/functions/setGlobalOptions.md
Expand Up @@ -50,9 +50,11 @@ setGlobalOptions({ options: { disableLowerIndexes: true, allowMixed: Severity.WA

Default: `false`

Set if caching should be disabled.
Set if global caching should be disabled.

Enabling this will disable cache (will not clear cache if already something is added).
Enabling this will disable cache globally (will not clear cache if already something is added).

If only locally disabled cache is needed, see [`@modelOptions`](../decorators/modelOptions.md)'s [`disableCaching`](../decorators/modelOptions.md#disablecaching) which does *not* have much of the following effects.

Effects:

Expand Down
Expand Up @@ -3,6 +3,13 @@ id: models-with-same-name
title: 'Models with same name'
---

<!--MDX Import section-->

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

<!--END MDX Import section-->

This Guide shows all the possibilities for a model to use different names.

:::note
Expand Down Expand Up @@ -68,9 +75,10 @@ expect(model.modelName).to.be.equal('CustomNameOption_CustomName');

## Disable Caching

Since Typegoose `10.2.0` there is also the option of disabling the cache with [global options](../../api/functions/setGlobalOptions.md#disablecaching).
Since Typegoose `10.2.0` there is also the option of disabling the cache globally with [global options](../../api/functions/setGlobalOptions.md#disablecaching) or locally via [`@modelOptions`](../../api/decorators/modelOptions.md#disablecaching).

Example:
<Tabs groupId="caching-global-local">
<TabItem value="global" label="Disable Cache globally">

```ts
import { setGlobalOptions } from "@typegoose/typegoose";
Expand Down Expand Up @@ -106,9 +114,51 @@ const KittenModelCon1 = getModelForClass(Kitten, { existingConnection: mongoose.
}
```

</TabItem>
<TabItem value="local" label="Disable Cache locally" default>

```ts
import { modelOptions } from "@typegoose/typegoose";

@modelOptions({ options: { disableCache: true } })
class Kitten {
@prop()
public name: string;
}

const KittenModelDefault = getModelForClass(Kitten);
const KittenModelCon1 = getModelForClass(Kitten, { existingConnection: mongoose.createConnection() });

// OR
{
class Kitten {
@prop()
public name: string;
}

const KittenModel = getModelForClass(Kitten, { options: { disableCaching: true } });
assert.ok(!!KittenModel.schema.path('name'));
}
{
class Kitten {
@prop()
public nameTag: string;
}

const KittenModel = getModelForClass(Kitten, { existingConnection: mongoose.createConnection(), options: { disableCaching: true } }); // still requires being defined on a different connection / mongoose instance
assert.ok(!!KittenModel.schema.path('nameTag'));
}
```

</TabItem>
</Tabs>

:::note
Models still cannot be defined more than once in the same connection / mongoose instance.
:::
:::caution
Setting the Cache to be disabled globally will make some functions that rely on it error, see [`E033`](../error-warning-details.md#cache-disabled-e033) and [`disableCaching`](../../api/functions/setGlobalOptions.md#disablecaching) which effects it will have.
:::

## Notes

Expand Down
6 changes: 6 additions & 0 deletions src/internal/constants.ts
Expand Up @@ -69,3 +69,9 @@ export enum Severity {
WARN,
ERROR,
}

/**
* Symbol to track if options have already been merged
* This is to reduce the "merge*" calls, which dont need to be run often if already done
*/
export const AlreadyMerged = Symbol('MOAlreadyMergedOptions');
17 changes: 9 additions & 8 deletions src/internal/schema.ts
Expand Up @@ -14,7 +14,7 @@ import type {
QueryMethodMap,
VirtualPopulateMap,
} from '../types';
import { DecoratorKeys } from './constants';
import { AlreadyMerged, DecoratorKeys } from './constants';
import { constructors } from './data';
import { NoDiscriminatorFunctionError, PathNotInSchemaError } from './errors';
import { processProp } from './processProp';
Expand All @@ -26,7 +26,7 @@ import {
getName,
isCachingEnabled,
isNullOrUndefined,
mergeSchemaOptions,
mergeMetadata,
} from './utils';

/**
Expand All @@ -44,7 +44,7 @@ import {
export function _buildSchema<U extends AnyParamConstructor<any>>(
cl: U,
origSch?: mongoose.Schema<any>,
opt?: mongoose.SchemaOptions,
opt?: IModelOptions,
isFinalSchema: boolean = true,
overwriteNaming?: INamingOptions,
extraOptions?: IBuildSchemaOptions
Expand All @@ -54,16 +54,17 @@ export function _buildSchema<U extends AnyParamConstructor<any>>(
assignGlobalModelOptions(cl); // to ensure global options are applied to the current class

// Options sanity check
opt = mergeSchemaOptions(isNullOrUndefined(opt) || typeof opt !== 'object' ? {} : opt, cl);
const rawOptions = typeof opt === 'object' ? opt : {};
const mergedOptions: IModelOptions = rawOptions?.[AlreadyMerged] ? rawOptions : mergeMetadata(DecoratorKeys.ModelOptions, rawOptions, cl);
mergedOptions[AlreadyMerged] = true;

const finalName = getName(cl, overwriteNaming);

logger.debug('_buildSchema Called for %s with options:', finalName, opt);
logger.debug('_buildSchema Called for %s with options:', finalName, mergedOptions);

/** Simplify the usage */
const Schema = mongoose.Schema;
const ropt: IModelOptions = Reflect.getMetadata(DecoratorKeys.ModelOptions, cl) ?? {};
const schemaOptions = Object.assign({}, ropt?.schemaOptions ?? {}, opt);
const schemaOptions = mergedOptions.schemaOptions ?? {};

const decorators = Reflect.getMetadata(DecoratorKeys.PropCache, cl.prototype) as DecoratedPropertyMetadataMap;

Expand Down Expand Up @@ -185,7 +186,7 @@ export function _buildSchema<U extends AnyParamConstructor<any>>(
});
}

if (isCachingEnabled()) {
if (isCachingEnabled(mergedOptions.options?.disableCaching)) {
// add the class to the constructors map
constructors.set(finalName, cl);
}
Expand Down
15 changes: 12 additions & 3 deletions src/internal/utils.ts
Expand Up @@ -157,7 +157,7 @@ export function getCachedSchema(target: AnyParamConstructor<any>): Record<string
export function getClass(
input: mongoose.Document | IObjectWithTypegooseFunction | { typegooseName: string } | string | any
): NewableFunction | undefined {
assertion(isCachingEnabled(), () => new CacheDisabledError('getClass'));
assertion(isGlobalCachingEnabled(), () => new CacheDisabledError('getClass'));

if (typeof input === 'string') {
return constructors.get(input);
Expand Down Expand Up @@ -744,9 +744,18 @@ export function mapModelOptionsToNaming(options: IModelOptions | undefined): INa
}

/**
* Helper function to check if caching is enabled
* Helper function to check if caching is enabled globally
* @returns "true" if caching is enabled or "false" if disabled
*/
export function isCachingEnabled(): boolean {
export function isGlobalCachingEnabled(): boolean {
return !(globalOptions.globalOptions?.disableCaching === true);
}

/**
* Helper function to check if caching is enabled globally AND by options
* @param opt The caching option (from IModelOptions)
* @returns "true" if caching is enabled or "false" if disabled
*/
export function isCachingEnabled(opt: boolean | undefined): boolean {
return isGlobalCachingEnabled() && !(opt === true);
}
33 changes: 16 additions & 17 deletions src/typegoose.ts
Expand Up @@ -7,10 +7,10 @@ import {
assertionIsClass,
getName,
isCachingEnabled,
isGlobalCachingEnabled,
isNullOrUndefined,
mapModelOptionsToNaming,
mergeMetadata,
mergeSchemaOptions,
warnNotMatchingExisting,
} from './internal/utils';

Expand All @@ -29,7 +29,7 @@ if (!isNullOrUndefined(process?.version) && !isNullOrUndefined(mongoose?.version
}

import { parseENV, setGlobalOptions } from './globalOptions';
import { DecoratorKeys } from './internal/constants';
import { AlreadyMerged, DecoratorKeys } from './internal/constants';
import { constructors, models } from './internal/data';
import { _buildSchema } from './internal/schema';
import { logger } from './logSettings';
Expand Down Expand Up @@ -68,12 +68,6 @@ export { Severity, PropType } from './internal/constants';

parseENV(); // call this before anything to ensure they are applied

/**
* Symbol to track if options have already been merged
* This is to reduce the "merge*" calls, which dont need to be run often if already done
*/
const AlreadyMerged = Symbol('MOAlreadyMergedOptions');

/**
* Build a Model From a Class
* @param cl The Class to build a Model from
Expand All @@ -96,7 +90,7 @@ export function getModelForClass<U extends AnyParamConstructor<any>, QueryHelper
mergedOptions[AlreadyMerged] = true;
const name = getName(cl, overwriteNaming);

if (isCachingEnabled() && models.has(name)) {
if (isCachingEnabled(mergedOptions.options?.disableCaching) && models.has(name)) {
return models.get(name) as ReturnModelType<U, QueryHelpers>;
}

Expand All @@ -110,6 +104,7 @@ export function getModelForClass<U extends AnyParamConstructor<any>, QueryHelper
return addModelToTypegoose<U, QueryHelpers>(compiledModel, cl, {
existingMongoose: mergedOptions?.existingMongoose,
existingConnection: mergedOptions?.existingConnection,
disableCaching: mergedOptions.options?.disableCaching,
});
}

Expand All @@ -127,7 +122,7 @@ export function getModelWithString<U extends AnyParamConstructor<any>, QueryHelp
key: string
): undefined | ReturnModelType<U, QueryHelpers> {
assertion(typeof key === 'string', () => new ExpectedTypeError('key', 'string', key));
assertion(isCachingEnabled(), () => new CacheDisabledError('getModelWithString'));
assertion(isGlobalCachingEnabled(), () => new CacheDisabledError('getModelWithString'));

return models.get(key) as any;
}
Expand All @@ -154,7 +149,9 @@ export function buildSchema<U extends AnyParamConstructor<any>>(
logger.debug('buildSchema called for "%s"', getName(cl, overwriteNaming));

// dont re-run the merging if already done so before (like in getModelForClass)
const mergedOptions = options?.[AlreadyMerged] ? options?.schemaOptions : mergeSchemaOptions(options?.schemaOptions, cl);
const rawOptions = typeof options === 'object' ? options : {};
const mergedOptions: IModelOptions = rawOptions?.[AlreadyMerged] ? rawOptions : mergeMetadata(DecoratorKeys.ModelOptions, rawOptions, cl);
mergedOptions[AlreadyMerged] = true;

let sch: mongoose.Schema<DocumentType<InstanceType<U>>> | undefined = undefined;
/** Parent Constructor */
Expand Down Expand Up @@ -213,15 +210,15 @@ export function buildSchema<U extends AnyParamConstructor<any>>(
export function addModelToTypegoose<U extends AnyParamConstructor<any>, QueryHelpers = BeAnObject>(
model: mongoose.Model<any>,
cl: U,
options?: { existingMongoose?: mongoose.Mongoose; existingConnection?: any }
options?: { existingMongoose?: mongoose.Mongoose; existingConnection?: any; disableCaching?: boolean }
) {
const mongooseModel = options?.existingMongoose?.Model || options?.existingConnection?.base?.Model || mongoose.Model;

assertion(model.prototype instanceof mongooseModel, new NotValidModelError(model, 'addModelToTypegoose.model'));
assertionIsClass(cl);

// only check cache after the above checks, just to make sure they run
if (!isCachingEnabled()) {
if (!isCachingEnabled(options?.disableCaching)) {
logger.info('Caching is not enabled, skipping adding');

return model as ReturnModelType<U, QueryHelpers>;
Expand Down Expand Up @@ -261,7 +258,7 @@ export function addModelToTypegoose<U extends AnyParamConstructor<any>, QueryHel
*/
export function deleteModel(name: string) {
assertion(typeof name === 'string', () => new ExpectedTypeError('name', 'string', name));
assertion(isCachingEnabled(), () => new CacheDisabledError('deleteModelWithClass'));
assertion(isGlobalCachingEnabled(), () => new CacheDisabledError('deleteModelWithClass'));

logger.debug('Deleting Model "%s"', name);

Expand All @@ -288,7 +285,7 @@ export function deleteModel(name: string) {
*/
export function deleteModelWithClass<U extends AnyParamConstructor<any>>(cl: U) {
assertionIsClass(cl);
assertion(isCachingEnabled(), () => new CacheDisabledError('deleteModelWithClass'));
assertion(isGlobalCachingEnabled(), () => new CacheDisabledError('deleteModelWithClass'));

let name = getName(cl);

Expand Down Expand Up @@ -435,7 +432,7 @@ export function getDiscriminatorModelForClass<U extends AnyParamConstructor<any>
mergedOptions[AlreadyMerged] = true;
const name = getName(cl, overwriteNaming);

if (isCachingEnabled() && models.has(name)) {
if (isCachingEnabled(mergedOptions.options?.disableCaching) && models.has(name)) {
return models.get(name) as ReturnModelType<U, QueryHelpers>;
}

Expand Down Expand Up @@ -464,7 +461,9 @@ export function getDiscriminatorModelForClass<U extends AnyParamConstructor<any>
mergePlugins,
});

return addModelToTypegoose<U, QueryHelpers>(compiledModel, cl);
return addModelToTypegoose<U, QueryHelpers>(compiledModel, cl, {
disableCaching: mergedOptions.options?.disableCaching,
});
}

/**
Expand Down
9 changes: 8 additions & 1 deletion src/types.ts
Expand Up @@ -479,6 +479,13 @@ export interface ICustomOptions {
* This option can be used over the prop-option to not have to re-define discriminators if used in multiple classes
*/
discriminators?: NestedDiscriminatorsFunction;
/**
* Disable Caching for this Class if defined via `@modelOptions`, or disable caching for the `getModelForClass` / `buildSchema` / `getDiscriminatorModelForClass`
* Does NOT overwrite global disabled caching
* "undefined" and "false" have the same meaning
* @default false
*/
disableCaching?: boolean;
}

/** Extra options for "_buildSchema" in "schema.ts" */
Expand Down Expand Up @@ -630,7 +637,7 @@ export interface IGlobalOptions {

export interface ITypegooseOptions {
/**
* Option to disable caching
* Option to disable caching globally
* completely disables the "constructors" and "models" maps
* "false" and "undefined" have the same result of enabling caching
* @default false
Expand Down

0 comments on commit 78ac3bc

Please sign in to comment.