Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (types): add typescript definitions and tests #91

Merged
merged 10 commits into from
Feb 28, 2022
18 changes: 14 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
"version": "6.2.0",
"description": "A plugin for Fastify to enable management of cache control headers",
"main": "plugin.js",
"types": "plugin.d.ts",
"scripts": {
"lint": "standard --verbose | snazzy",
"lint:ci": "standard",
"test": "tap test/*.test.js"
"test": "npm run unit && npm run test:types",
"test:types": "tsd",
"unit": "tap -J \"test/*.test.js\"",
"unit:report": "npm run unit -- --coverage-report=html",
"unit:verbose": "npm run unit -- -Rspec"
},
"precommit": [
"lint",
Expand All @@ -28,15 +33,20 @@
},
"homepage": "https://github.com/fastify/fastify-caching#readme",
"devDependencies": {
"fastify": "^3.24.1",
"@types/node": "^17.0.21",
"fastify": "^3.27.2",
"pre-commit": "^1.2.2",
"snazzy": "^9.0.0",
"standard": "^16.0.4",
"tap": "^15.1.5"
"tap": "^15.1.6",
"tsd": "^0.19.1"
},
"dependencies": {
"abstract-cache": "^1.0.1",
"fastify-plugin": "^3.0.0",
"fastify-plugin": "^3.0.1",
"uid-safe": "^2.1.5"
},
"tsd": {
"directory": "test/types"
}
}
124 changes: 124 additions & 0 deletions plugin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/// <reference types='node' />

import { FastifyPluginCallback } from 'fastify';

declare module 'fastify' {
interface FastifyInstance {
cache: AbstractCacheCompliantObject;
cacheSegment: string;
etagMaxLife: number | undefined;
}

interface FastifyReply {
/**
* This method allows setting of the `expires` header.
*
* @link [reply.expires() documentation](https://github.com/fastify/fastify-caching#replyexpiresdate)
*
* @param date A regular `Date` object, or a valid date string according to [RFC 2616 section 14.21](https://datatracker.ietf.org/doc/html/rfc2616#section-14.21).
*/
expires(date?: Date): this;

/**
* This method allows setting of the `etag` header.
*
* @link [reply.etag() documentation](https://github.com/fastify/fastify-caching#replyetagstring-number)
*
* @param tag Any arbitrary string that is valid for HTTP headers.
* @param timeToLive The time must be specified in milliseconds. The default lifetime, when the parameter is not specified, is `3600000`.
*/
etag(tag?: string, timeToLive?: number): this;
}
}

/**
* @link [`abstract-cache` protocol documentation](https://github.com/jsumners/abstract-cache#protocol)
*/
export interface AbstractCacheCompliantObject {
get(
key: string | { id: string; segment: string },
callback?: (error: unknown, result: unknown) => void
): void;
set(
key: string | { id: string; segment: string },
value: unknown,
timeToLive: number,
callback?: (error: unknown, result: unknown) => void
): void;
}

export interface Privacy {
NOCACHE: 'no-cache';
PRIVATE: 'private';
PUBLIC: 'public';
}

/**
* @link [`fastify-caching` options documentation](https://github.com/fastify/fastify-caching#options)
*/
export interface FastifyCachingPluginOptions {
/**
* An [abstract-cache](https://www.npmjs.com/package/abstract-cache) protocol compliant cache object.
* Note: the plugin requires a cache instance to properly support the ETag mechanism.
* Therefore, if a falsy value is supplied the default will be used.
*
* - Default value: `abstract-cache.memclient`
*/
cache?: AbstractCacheCompliantObject;

/**
* The segment identifier to use when communicating with the cache.
*
* - Default value: `fastify-caching`
*/
cacheSegment?: string;

etagMaxLife?: number;

/**
* A value, in seconds, for the max-age the resource may be cached.
* When this is set, and privacy is not set to no-cache, then ', max-age=<value>'
* will be appended to the cache-control header.
*
* - Default value: `undefined`
*/
expiresIn?: number;

/**
* It can be set to any string that is valid for a cache-response-directive as
* defined by [RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616#section-14.9).
*
* - Default value: `undefined`
*
* @link [MDN Cache-Control - Response Directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives)
*
* @example
* const fastifyCaching = require('fastify-caching');
*
* // Disabling client side caching of all routes.
* fastify.register(fastifyCaching, { privacy: fastifyCaching.privacy.NOCACHE });
*/
privacy?: string;

/**
* A value, in seconds, for the length of time the resource is fresh and may be
* held in a shared cache (e.g. a CDN). Shared caches will ignore max-age when
* this is specified, though browsers will continue to use max-age. Should be
* used with expiresIn, not in place of it. When this is set, and privacy is set
* to public, then ', s-maxage=<value>' will be appended to the cache-control header.
*
* - Default value: `undefined`
*/
serverExpiresIn?: number;
}

declare const fastifyCaching: FastifyPluginCallback<FastifyCachingPluginOptions> & {
privacy: Privacy;
};

declare const privacy: {
privacy: Privacy;
};

export default fastifyCaching;
export { fastifyCaching, privacy };
52 changes: 52 additions & 0 deletions test/types/plugin.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Fastify, { FastifyReply } from 'fastify';
import { expectAssignable, expectError, expectType } from 'tsd';
import fastifyCaching, {
AbstractCacheCompliantObject,
FastifyCachingPluginOptions,
} from '../..';

const fastify = Fastify({ logger: true });

const cachingOptions: FastifyCachingPluginOptions = {
privacy: fastifyCaching.privacy.PUBLIC,
expiresIn: 300,
cacheSegment: 'fastify-caching',
};
expectAssignable<FastifyCachingPluginOptions>(cachingOptions);

fastify.register(fastifyCaching, cachingOptions);

expectType<AbstractCacheCompliantObject>(fastify.cache);
expectType<AbstractCacheCompliantObject['get']>(fastify.cache.get);
expectType<AbstractCacheCompliantObject['set']>(fastify.cache.set);
expectType<string>(fastify.cacheSegment);
// expectType<number>(fastify.etagMaxLife);

fastify.get('/one', async (request, reply) => {
expectType<(tag?: string, timeToLive?: number) => FastifyReply>(reply.etag);
expectType<(date?: Date) => FastifyReply>(reply.expires);

expectType<FastifyReply>(reply.etag('hello', 6000));
expectType<FastifyReply>(reply.expires(new Date(Date.now() + 6000)));

return { message: 'one' };
});

fastify.get('/two', async (request, reply) => {
expectType<FastifyReply>(
reply.etag('hello', 6000).expires(new Date(Date.now() + 6000))
);

return { message: 'two' };
});

// We register a new instance that should trigger a typescript error.
const shouldErrorApp = Fastify({ logger: true });

const badCachingOptions = {
privacy: fastifyCaching.privacy.PRIVATE,
expiresIn: 'a string instead of a number of second',
cacheSegment: 'fastify-caching',
};

expectError(shouldErrorApp.register(fastifyCaching, badCachingOptions));