Skip to content

Commit 2ae09a3

Browse files
committed
built our core code
1 parent 2c30e79 commit 2ae09a3

7 files changed

Lines changed: 4062 additions & 11 deletions

File tree

package.json

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,37 @@
66
"repository": "https://github.com/andrejpk/apollo-datasource-cosmosdb",
77
"author": "Andrej Kyselica",
88
"license": "MIT",
9+
"keywords": [
10+
"apollo",
11+
"graphql",
12+
"datasource",
13+
"azure",
14+
"cosmosdb",
15+
"cosmos"
16+
],
917
"scripts": {
1018
"build": "tsc",
1119
"watch": "tsc --watch"
1220
},
1321
"devDependencies": {
22+
"@azure/cosmos": "^3.9.3",
23+
"@types/node": "^14.14.10",
24+
"graphql": "^15.4.0",
25+
"jest": "^26.6.3",
26+
"prettier": "^2.2.1",
1427
"typescript": "^4.1.2"
15-
}
16-
}
28+
},
29+
"peerDependencies": {
30+
"@azure/cosmos": "^3.9.3"
31+
},
32+
"dependencies": {
33+
"apollo-datasource": "^0.7.2",
34+
"apollo-server-caching": "^0.5.2",
35+
"apollo-server-errors": "^2.4.2",
36+
"bson": "^4.2.0",
37+
"dataloader": "^2.0.0"
38+
},
39+
"files": [
40+
"/dist"
41+
]
42+
}

src/cache.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Container, Operation } from "@azure/cosmos";
2+
import { KeyValueCache } from "apollo-server-caching";
3+
import DataLoader from "dataloader";
4+
import { EJSON } from "bson";
5+
6+
// https://github.com/graphql/dataloader#batch-function
7+
const orderDocs = <V>(ids: readonly string[]) => (
8+
docs: (V | undefined)[],
9+
keyFn?: (source: V) => string
10+
) => {
11+
const keyFnDef =
12+
keyFn ||
13+
((source: V & { id?: string }) => {
14+
if (source.id) return source.id;
15+
throw new Error(
16+
"Could not find ID for object; if using an alternate key, pass in a key function"
17+
);
18+
});
19+
20+
const checkNotUndefined = (input: V | undefined): input is V => {
21+
return Boolean(input);
22+
};
23+
24+
const idMap: Record<string, V> = docs
25+
.filter(checkNotUndefined)
26+
.reduce((prev: Record<string, V>, cur: V) => {
27+
prev[keyFnDef(cur)] = cur;
28+
return prev;
29+
}, {});
30+
return ids.map((id) => idMap[id]);
31+
};
32+
33+
export interface createCatchingMethodArgs {
34+
container: Container;
35+
cache: KeyValueCache;
36+
}
37+
38+
export interface FindArgs {
39+
ttl?: number;
40+
}
41+
42+
export interface CachedMethods<DType> {
43+
findOneById: (id: string, args: FindArgs) => Promise<DType | undefined>;
44+
findManyByIds: (
45+
ids: string[],
46+
args: FindArgs
47+
) => Promise<(DType | undefined)[]>;
48+
deleteFromCacheById: (id: string) => Promise<void>;
49+
}
50+
51+
export const createCachingMethods = <DType>({
52+
container,
53+
cache,
54+
}: createCatchingMethodArgs): CachedMethods<DType> => {
55+
const loader = new DataLoader<string, DType>(async (ids) => {
56+
const operations = ids.map<Operation>((id) => ({
57+
operationType: "Read",
58+
id,
59+
}));
60+
const response = await container.items.bulk(operations);
61+
const responseDocs = response.map((r) =>
62+
r.resourceBody ? ((r.resourceBody as unknown) as DType) : undefined
63+
);
64+
return orderDocs<DType>(ids)(responseDocs);
65+
});
66+
67+
const cachePrefix = `cosmos-${container.url}-`;
68+
69+
const methods: CachedMethods<DType> = {
70+
findOneById: async (id, { ttl } = {}) => {
71+
const key = cachePrefix + id;
72+
73+
const cacheDoc = await cache.get(key);
74+
if (cacheDoc) {
75+
return EJSON.parse(cacheDoc) as DType;
76+
}
77+
78+
const doc = await loader.load(id);
79+
80+
if (Number.isInteger(ttl)) {
81+
cache.set(key, EJSON.stringify(doc), { ttl });
82+
}
83+
84+
return doc;
85+
},
86+
87+
findManyByIds: (ids, args = {}) =>
88+
Promise.all(ids.map((id) => methods.findOneById(id, args))),
89+
90+
deleteFromCacheById: async (id) => {
91+
loader.clear(id);
92+
await cache.delete(cachePrefix + id);
93+
},
94+
};
95+
96+
return methods;
97+
};

src/datasource.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { DataSource } from "apollo-datasource";
2+
import { ApolloError } from "apollo-server-errors";
3+
import { InMemoryLRUCache, KeyValueCache } from "apollo-server-caching";
4+
import { Container } from "@azure/cosmos";
5+
6+
import { isCosmosDbContainer } from "./helpers";
7+
import { createCachingMethods, CachedMethods } from "./cache";
8+
9+
export const placeholderHandler = () => {
10+
throw new Error("DataSource not initialized");
11+
};
12+
13+
export class CosmosDataSource<TData, TContext = any>
14+
extends DataSource<TContext>
15+
implements CachedMethods<TData> {
16+
container: Container;
17+
context?: TContext;
18+
// these get set by the initializer but they must be defined or nullable after the constructor
19+
// runs, so we guard against using them before init
20+
findOneById = placeholderHandler;
21+
findManyByIds = placeholderHandler;
22+
deleteFromCacheById = placeholderHandler;
23+
24+
constructor(container: Container) {
25+
super();
26+
27+
if (!isCosmosDbContainer(container)) {
28+
throw new ApolloError(
29+
"CosmosDataSource must be created with a CosmosDb container (from @azure/cosmos)"
30+
);
31+
}
32+
33+
this.container = container;
34+
}
35+
36+
initialize({
37+
context,
38+
cache,
39+
}: { context?: TContext; cache?: KeyValueCache } = {}) {
40+
this.context = context;
41+
42+
const methods = createCachingMethods<TData>({
43+
container: this.container,
44+
cache: cache || new InMemoryLRUCache(),
45+
});
46+
47+
Object.assign(this, methods);
48+
}
49+
}

src/helpers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Container } from "@azure/cosmos";
2+
3+
export const isCosmosDbContainer = (
4+
maybeContainer: any
5+
): maybeContainer is Container => {
6+
// does the duck quack?
7+
return (
8+
maybeContainer.url &&
9+
maybeContainer.items &&
10+
maybeContainer.database &&
11+
maybeContainer.getPartitionKeyDefinition
12+
);
13+
};

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { CosmosDataSource } from "./datasource";
2+
3+
export default { CosmosDataSource };

tsconfig.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
{
22
"compilerOptions": {
33
/* Basic Options */
4-
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
5-
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
4+
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
5+
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
66
// "lib": [], /* Specify library files to be included in the compilation. */
77
// "allowJs": true, /* Allow javascript files to be compiled. */
88
// "checkJs": true, /* Report errors in .js files. */
99
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
1010
// "declaration": true, /* Generates corresponding '.d.ts' file. */
1111
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
12-
// "sourceMap": true, /* Generates corresponding '.map' file. */
12+
"sourceMap": true /* Generates corresponding '.map' file. */,
1313
// "outFile": "./", /* Concatenate and emit output to single file. */
14-
"outDir": "./dist", /* Redirect output structure to the directory. */
15-
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
14+
"outDir": "./dist" /* Redirect output structure to the directory. */,
15+
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
1616
// "composite": true, /* Enable project compilation */
1717
// "removeComments": true, /* Do not emit comments to output. */
1818
// "noEmit": true, /* Do not emit outputs. */
@@ -21,7 +21,7 @@
2121
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
2222

2323
/* Strict Type-Checking Options */
24-
"strict": true, /* Enable all strict type-checking options. */
24+
"strict": true /* Enable all strict type-checking options. */,
2525
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
2626
// "strictNullChecks": true, /* Enable strict null checks. */
2727
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
@@ -36,14 +36,14 @@
3636
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
3737

3838
/* Module Resolution Options */
39-
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
39+
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
4040
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
4141
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
4242
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
4343
// "typeRoots": [], /* List of folders to include type definitions from. */
4444
// "types": [], /* Type declaration files to be included in compilation. */
4545
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
46-
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
46+
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
4747
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
4848

4949
/* Source Map Options */
@@ -56,4 +56,4 @@
5656
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
5757
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
5858
}
59-
}
59+
}

0 commit comments

Comments
 (0)