diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..94413917 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,48 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + 'eslint:recommended', + // 'plugin:@typescript-eslint/recommended', + // Going with type checked because of https://typescript-eslint.io/rules/no-floating-promises + 'plugin:@typescript-eslint/recommended-type-checked', + ], + overrides: [ + { + env: { + node: true, + }, + files: ['.eslintrc.{js,cjs}'], + parserOptions: { + sourceType: 'script', + }, + }, + ], + globals: { + _: true, // TODO: replace with explicit imports + Meteor: true, + Npm: true, + Package: true, + Mongo: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: true, + }, + plugins: ['@typescript-eslint'], + ignorePatterns: ['test*'], + rules: { + // For now + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-this-alias': 'off', + }, +}; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87db41da..c273926d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,23 +4,27 @@ on: [push, pull_request] jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - meteor: [2.3.1, 2.6.1, 2.7.3, 2.8.1, 2.9.1, 2.12, 2.14] + meteor: [3.0.1] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - name: Install Dependencies + run: | + curl https://install.meteor.com | /bin/sh - - name: Setup Meteor - uses: meteorengineer/setup-meteor@v1 - with: - meteor-release: ${{ matrix.meteor }} - name: Setup tests run: | meteor create --release ${{ matrix.meteor }} --bare test cd test - meteor npm i --save selenium-webdriver@3.6.0 chromedriver@2.36.0 simpl-schema@1.13.1 chai + meteor npm i --save puppeteer chai + mkdir packages + cd packages + git clone -b update/meteor-3.0 https://github.com/bhunjadi/denormalize.git + - name: Test working-directory: ./test - run: METEOR_PACKAGE_DIRS="../" TEST_BROWSER_DRIVER=chrome meteor test-packages --once --driver-package meteortesting:mocha ../ + run: METEOR_PACKAGE_DIRS="../" TEST_BROWSER_DRIVER=puppeteer meteor test-packages --once --driver-package meteortesting:mocha ../ diff --git a/.npm/package/npm-shrinkwrap.json b/.npm/package/npm-shrinkwrap.json index 5c67106f..3499ff95 100644 --- a/.npm/package/npm-shrinkwrap.json +++ b/.npm/package/npm-shrinkwrap.json @@ -1,5 +1,5 @@ { - "lockfileVersion": 1, + "lockfileVersion": 4, "dependencies": { "assertion-error": { "version": "1.1.0", @@ -22,9 +22,9 @@ "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==" }, "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==" }, "commander": { "version": "2.20.3", @@ -34,7 +34,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "deep-eql": { "version": "3.0.1", @@ -54,22 +54,22 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==" }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==" + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==" }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==" }, "inherits": { "version": "2.0.4", @@ -79,7 +79,7 @@ "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, "minimatch": { "version": "3.1.2", @@ -89,12 +89,12 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==" }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "pathval": { "version": "1.1.1", @@ -107,14 +107,14 @@ "integrity": "sha512-FrKLXaUad4IYEpIzs9BAaXXNwcRnzg2vPfPTDgPRrKncMhgx9wftFzJrIRh9SCxxz0zHgvSKULQRRGA9JQWcZQ==" }, "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==" }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" } } } diff --git a/.versions b/.versions index 1fa61300..68dd9551 100644 --- a/.versions +++ b/.versions @@ -1,64 +1,61 @@ -allow-deny@1.1.1 -babel-compiler@7.10.5 -babel-runtime@1.5.1 -base64@1.0.12 -binary-heap@1.0.11 -boilerplate-generator@1.7.2 -caching-compiler@1.2.2 -callback-hook@1.5.1 -check@1.3.2 -coffeescript@2.4.1 -coffeescript-compiler@2.4.1 -cultofcoders:grapher@1.5.0 -dburles:mongo-collection-instances@0.4.0 -ddp@1.4.1 -ddp-client@2.6.1 -ddp-common@1.4.0 -ddp-server@2.7.0 -diff-sequence@1.1.2 -dynamic-import@0.7.3 -ecmascript@0.16.8 -ecmascript-runtime@0.8.1 -ecmascript-runtime-client@0.12.1 -ecmascript-runtime-server@0.11.0 -ejson@1.1.3 -fetch@0.1.4 -geojson-utils@1.0.11 -herteby:denormalize@0.6.7 -id-map@1.1.1 -inter-process-messaging@0.1.1 -lai:collection-extensions@0.4.0 -local-test:cultofcoders:grapher@1.5.0 -logging@1.3.3 -matb33:collection-hooks@1.3.1 -meteor@1.11.5 +allow-deny@2.0.0 +babel-compiler@7.11.0 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.0 +callback-hook@1.6.0 +check@1.4.2 +core-runtime@1.0.0 +cultofcoders:grapher@2.0.0-beta.1 +dburles:mongo-collection-instances@1.0.0 +ddp@1.4.2 +ddp-client@3.0.0 +ddp-common@1.4.3 +ddp-server@3.0.0 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.9 +ecmascript-runtime@0.8.2 +ecmascript-runtime-client@0.12.2 +ecmascript-runtime-server@0.11.1 +ejson@1.1.4 +facts-base@1.0.2 +fetch@0.1.5 +geojson-utils@1.0.12 +herteby:denormalize@0.7.0-beta.1 +id-map@1.2.0 +inter-process-messaging@0.1.2 +lai:collection-extensions@1.0.0 +local-test:cultofcoders:grapher@2.0.0-beta.1 +logging@1.3.5 +matb33:collection-hooks@2.0.0 +meteor@2.0.1 meteortesting:browser-tests@0.1.2 meteortesting:mocha@0.4.4 -minimongo@1.9.3 -modern-browsers@0.1.10 -modules@0.20.0 -modules-runtime@0.13.1 -mongo@1.16.8 -mongo-decimal@0.1.3 -mongo-dev-server@1.1.0 -mongo-id@1.0.8 -npm-mongo@4.17.2 -ordered-dict@1.1.0 -peerlibrary:extend-publish@0.6.0 -peerlibrary:subscription-scope@0.5.0 +minimongo@2.0.1 +modern-browsers@0.1.11 +modules@0.20.1 +modules-runtime@0.13.2 +mongo@2.0.1 +mongo-decimal@0.1.4-beta300.7 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@4.17.4 +ordered-dict@1.2.0 practicalmeteor:mocha-core@1.0.1 -promise@0.12.2 -random@1.2.1 -react-fast-refresh@0.2.8 -reactive-var@1.0.12 -reload@1.3.1 -retry@1.1.0 -reywood:publish-composite@1.8.8 -routepolicy@1.1.1 -socket-stream-client@0.5.2 -tracker@1.3.3 -typescript@4.9.5 -underscore@1.6.0 -webapp@1.13.8 -webapp-hashing@1.1.1 -zodern:types@1.0.11 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +reywood:publish-composite@1.8.12 +routepolicy@1.1.2 +socket-stream-client@0.5.3 +tracker@1.3.4 +typescript@5.4.3 +underscore@1.6.4 +webapp@2.0.1 +webapp-hashing@1.1.2 +zodern:types@1.0.13 diff --git a/@types/collection-hooks.d.ts b/@types/collection-hooks.d.ts new file mode 100644 index 00000000..49dff6e6 --- /dev/null +++ b/@types/collection-hooks.d.ts @@ -0,0 +1,164 @@ +declare module 'meteor/matb33:collection-hooks' { + import { Meteor } from 'meteor/meteor'; + type Options = { + fetchPrevious?: boolean; + [key: string]: any; + }; + type TGlobalOptions = { + all?: Options; + insert?: Options; + update?: Options; + upsert?: Options; + find?: Options; + findOne?: Options; + remove?: Options; + }; + interface CollectionHooks { + defaultUserId?: string; + directEnv?: Meteor.EnvironmentVariable; + GlobalOptions?: TGlobalOptions; + defaults?: { + all?: TGlobalOptions; + before?: TGlobalOptions; + after?: TGlobalOptions; + }; + } +} + +namespace Mongo { + import { CollectionHooks } from 'meteor/matb33:collection-hooks'; + type GenericFunction = (...args: any) => any; + type THookThis = { + _super: UnderlyingMethod; + context: ThisType; + args: Parameters; + transform: (doc: T) => T; + }; + type THookThisWithId = THookThis< + T, + UnderlyingMethod + > & { + _id: string; + }; + type THookThisWithTransform< + T, + UnderlyingMethod extends GenericFunction, + > = THookThis & { + transform: (doc: T) => T; + }; + type THookThisWithTransformAndPrevious< + T, + UnderlyingMethod extends GenericFunction, + > = THookThisWithTransform & { + previous: T; + }; + type THookBeforeInsert = ( + this: THookThis['insert']>, + userId: string | undefined, + doc: T, + ) => O; + type THookAfterInsert = ( + this: THookThisWithId['insert']>, + userId: string | undefined, + doc: T, + ) => O; + type THookBeforeUpdate = ( + this: THookThis['update']> & { + previous: T; + transform: (doc: T) => T; + }, + userId: string | undefined, + doc: T, + fieldNames: string[], + modifier: any, + options: any, + ) => O; + type THookAfterUpdate = ( + this: THookThisWithTransformAndPrevious['update']> & { + previous: T; + transform: (doc: T) => T; + }, + userId: string | undefined, + doc: T, + fieldNames: string[], + modifier: any, + options: any, + ) => O; + type THookRemove = ( + this: THookThisWithTransform['remove']>, + userId: string | undefined, + doc: T, + ) => O; + type THookUpsert = ( + this: THookThis['upsert']>, + userId: string | undefined, + selector: any, + modifier: any, + options: any, + ) => O; + type THookBeforeFind = ( + this: THookThis['find']>, + userId: string | undefined, + selector: any, + options: any, + ) => O; + type THookAfterFind = ( + this: THookThis['find']>, + userId: string | undefined, + selector: any, + options: any, + cursor: Cursor, + ) => void; + type THookBeforeFindOne = ( + this: THookThis['findOne']>, + userId: string | undefined, + selector: any, + options: any, + ) => O; + type THookAfterFindOne = ( + this: THookThis['findOne']>, + userId: string | undefined, + selector: any, + options: any, + doc: T, + ) => void; + type THandler = { + remove(): void; + replace(callback: F, options: any): void; + }; + + interface Collection { + hookOptions: CollectionHooks['GlobalOptions']; + direct: Pick< + Collection, + | 'insert' + | 'insertAsync' + | 'update' + | 'updateAsync' + | 'find' + | 'findOne' + | 'findOneAsync' + | 'remove' + | 'removeAsync' + >; + before: { + insert>(fn: Fn): THandler; + update>(fn: Fn): THandler; + remove>(fn: Fn): THandler; + upsert>(fn: Fn): THandler; + find>(fn: Fn): THandler; + findOne>(fn: Fn): THandler; + }; + after: { + insert>(fn: Fn): THandler; + update>( + fn: Fn, + options?: { fetchPrevious?: boolean }, + ): THandler; + remove>(fn: Fn): THandler; + upsert>(fn: Fn): THandler; + find>(fn: Fn): THandler; + findOne>(fn: Fn): THandler; + }; + } +} diff --git a/@types/index.d.ts b/@types/index.d.ts new file mode 100644 index 00000000..35868a95 --- /dev/null +++ b/@types/index.d.ts @@ -0,0 +1,309 @@ +declare module 'meteor/cultofcoders:grapher' { + export type LinkDenormalizeSchema = { + field: string; + body: unknown; + bypassSchema?: boolean; + }; + + export type LinkConfigType = 'one' | 'many' | '1' | '*'; + + export type LinkConfigCollection = Mongo.Collection; + + // Check: lib/links/config.schema.js:LinkConfigSchema + export type LinkConfig = { + // not needed for inversed links + type?: LinkConfigType; + + // Looks like intention is to support other collections, not just mongodb + collection: LinkConfigCollection | string; // TODO: define collection type + foreignIdentityField?: string; + field?: string; + metadata?: boolean; + inversedBy?: string; + index?: boolean; + unique?: boolean; + autoremove?: boolean; + denormalize?: unknown; + + // processed link config + relatedLinker?: Grapher.LinkerClass; + }; + + export type ProcessedDirectLink = Omit< + LinkConfig, + 'type' | 'field' | 'collection' + > & { + type: LinkConfigType; + field: string; + collection: LinkConfigCollection; + }; + + export type ProcessedReverseLink = Omit< + LinkConfig, + 'metadata' | 'inversedBy' | 'collection' + > & { + collection: LinkConfigCollection; + inversedBy: string; + metadata?: boolean; + relatedLinker: LinkerClass; + }; + + export type ProcessedLink = ProcessedDirectLink | ProcessedReverseLink; + + export type LinkConfigDefaults = Partial; + + export class LinkerClass { + constructor( + mainCollection: Mongo.Collection, + linkName: string, + linkConfig: LinkConfig, + ); + + linkStorageField: string | undefined; + linkConfig: ProcessedLink; + mainCollection: Mongo.Collection; + + createLink( + object: unknown, + collection?: Mongo.Collection | null, + ): LinkBaseClass; + + isVirtual(): boolean; + isSingle(): boolean; + } + + export type DefaultFiltersWithMeta = Mongo.Selector & { + $meta?: unknown; + }; + + export type LinkObject = { + _id: string; + [Key: string]: unknown; + }; + + export type BodyFields = { + [Key: string]: number | BodyFields; + }; + + // Idea is that we have $filters on T, i.e. the actual document. + // But body should be defined based on links. + // TODO: nesting? + export type Body = { + $options?: Options; + $filters?: Mongo.Query; + [Key: WithLinks]: number | Body; + }; + + export class LinkBaseClass { + constructor( + linker: LinkerClass, + object: LinkObject, + collection: Mongo.Collection, + ); + + get config(): LinkConfig; // processed + get isVirtual(): boolean; + object: LinkObject; + + // Stored link information value + value(): unknown; + + find( + filters?: DefaultFiltersWithMeta, + options?: Mongo.Options, + userId?: string, + ): Mongo.Cursor; // TODO: depends on passed-in fields + + fetch( + filters?: DefaultFiltersWithMeta, + options?: Mongo.Options, + userId?: string, + ): Promise; + + fetchAsArray( + filters?: DefaultFiltersWithMeta, + options?: Mongo.Options, + userId?: string, + ): Promise; + + add(what: unknown): Promise; + remove(what: unknown): Promise; + set(what: unknown, metadata?): Promise; + unset(): Promise; + metadata(): Promise; + + clean(): void; + } + + type BaseDocumentShape = { + _id?: string; + }; + + type IdSingleOption = string | BaseDocumentShape; + + type IdOption = IdSingleOption | IdSingleOption[]; + + type SmartArgumentsOptions = + | { + saveToDatabase: true; + collection: Mongo.Collection; + } + | { + saveToDatabase?: false; + }; + + export type ExposureFirewallFn = ( + filters: { [key: string]: unknown }, + options: Mongo.Options, + userId?: string, + ) => void | Promise; + + export type Params = { + [key: string]: unknown; + }; + + export type Filters = Mongo.Query; + export type Options = Mongo.Options; + + export type ValidateParamsFn = (params: P) => void | Promise; + export type ValidateParamsParam = boolean | ValidateParamsFn; + + export type QueryOptions = Mongo.Options & { + params?: P; + validateParams?: ValidateParamsParam; + }; + + export type QueryFetchContext = { + userId?: string; + $options?: Mongo.Options; + }; + + export type ExposureConfig = { + firewall?: ExposureFirewallFn | ExposureFirewallFn[]; + maxLimit?: number; + maxDepth?: number; + publication?: boolean; + method?: boolean; + blocking?: boolean; + body?: Body | boolean | ((userId: string | undefined) => Body); + restrictedFields?: string[]; + restrictLinks?: ((userId: string | undefined) => string[]) | string[]; + }; + + export class ExposureClass { + constructor( + collection: Mongo.Collection, + config?: ExposureConfig | ExposureFirewallFn, + ); + + collection: Mongo.Collection; + name: string; + config: ExposureConfig; + + // Sets global config + static setConfig(config: ExposureConfig): void; + // Gets global config + static getConfig(): ExposureConfig; + } + + export type DataCallback = (err: unknown, data: T) => void; + + export interface QueryBase { + // client-side + fetch(callback: DataCallback): unknown[]; + fetchAsync(): Promise; + // @deprecated + fetchSync(): Promise; + + fetchOneAsync(): Promise; + // @deprecated + fetchOneSync(): Promise; + + getCountAsync(): Promise; + // @deprecated + getCountSync(): Promise; + } + + export class QueryBaseClass implements QueryBase { + isGlobalQuery: boolean; + + constructor( + collection: Mongo.Collection, + body: Body, + options?: Options, + ); + } + + export class NamedQueryBaseClass { + isNamedQuery: boolean; + + constructor( + name: string, + collection: Mongo.Collection, + name: string, + body: Body, + options?: Options, + ); + + // client-side + fetch(callback: (err: unknown, res: unknown[]) => void): void; + fetchAsync(): Promise; + fetchOneAsync(): Promise; + getCountAsync(): Promise; + } + + export type HypernovaConfig

= { + params?: P; + bypassFirewalls?: boolean; + }; + + export type CountEndpointFunction = ( + request: unknown, + ) => Mongo.Cursor>; + + type MeteorSubscribeCallbacks = { + onStop(err?: unknown): void; + onReady(): void; + }; + + function createQuery( + name: string, + body: Body, + options?: QueryOptions, + ): NamedQueryBaseClass; + + function createQuery(body: Body, options?: QueryOptions): QueryBase; +} + +namespace Grapher { + export * from 'meteor/cultofcoders:grapher'; +} + +namespace Mongo { + interface Collection { + __links: Record; + __isExposedForGrapher?: boolean; + __exposure?: ExposureClass; + firewall?: ( + filters: Grapher.Filters, + options: Grapher.Options, + userId?: string, + ) => Promise; + + addLinks(links: Record): void; + getLinker(name: string): Grapher.LinkerClass | undefined; + getLink( + doc: unknown, + name: string, + ): Promise; + + expose(config?: Grapher.ExposureConfig | Grapher.ExposureFirewallFn): void; + + find>( + selector?: Selector | ObjectID | string, + options?: O, + userId?: string, + enforceMaxDepth?: boolean, + ): Cursor>; + } +} diff --git a/@types/meteor-fixes.d.ts b/@types/meteor-fixes.d.ts new file mode 100644 index 00000000..74e668d7 --- /dev/null +++ b/@types/meteor-fixes.d.ts @@ -0,0 +1,5 @@ +namespace Mongo { + interface Collection { + _name: string; + } +} diff --git a/@types/mongo-collection-instances.d.ts b/@types/mongo-collection-instances.d.ts new file mode 100644 index 00000000..a37a4625 --- /dev/null +++ b/@types/mongo-collection-instances.d.ts @@ -0,0 +1,5 @@ +namespace Mongo { + interface CollectionStatic { + get: (name: string) => Collection; + } +} diff --git a/@types/publish-composite.d.ts b/@types/publish-composite.d.ts new file mode 100644 index 00000000..1d199eb1 --- /dev/null +++ b/@types/publish-composite.d.ts @@ -0,0 +1,27 @@ +// Definitions by: Robbie Van Gorkom +// Matthew Zartman +// Jan Dvorak +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// Minimum TypeScript Version: 4.1 + +declare module 'meteor/reywood:publish-composite' { + import { Mongo } from 'meteor/mongo'; + + type RepeatingOptions = { + find: (topLevelDocument: object) => Mongo.Cursor; + children?: + | RepeatingOptions[] + | ((topLevelDocument: object) => RepeatingOptions[]); + collectionName?: string; + }; + + function publishComposite( + name: string, + options: + | RepeatingOptions + | (( + this: Meteor.MethodThisType, + body: Mongo.FieldSpecifier, + ) => RepeatingOptions), + ): void; +} diff --git a/@types/src/index.d.ts b/@types/src/index.d.ts deleted file mode 100644 index 96dcecf9..00000000 --- a/@types/src/index.d.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** @format */ - -/// - -declare module 'meteor/cultofcoders:grapher' { - import { Mongo } from 'meteor/mongo' - import { DocumentNode } from 'graphql' - - module Grapher { - type TypesEnum = 'one' | 'many' - - interface Query { - setParams(): any - resolve(): any - expose(): any - } // WIP - - interface ILink { - collection: Mongo.Collection - type: TypesEnum - metadata?: true - field: string - index?: boolean - denormalize?: iDenormalize - } - - interface ILink { - collection: Mongo.Collection - inversedBy: string - denormalize?: iDenormalize - } - - type Link = { - [field: string]: ILink - } - - type QueryOptions = { - $filter?: Mongo.FieldExpression - } - type Body = { - [field: string]: DependencyGraph | Body | QueryOptions - } - type createQuery = ( - name: string, - body: Body | {}, - options?: {} - ) => any - - type QueryBody = Body - - type BodyEnum = 0 | 1 - - type GrapherBody = TSchema extends object - ? SelectionSet> - : SelectionSet - - type TEmbodyArgs = { - body: GrapherBody - getArgs(): TArgs - } - - type DependencyGraph = { - [field: string]: GrapherBody | DependencyGraph - } - - type TFirewall = ( - filters: TFilters, - options: TOptions, - userId: string - ) => void - - interface SelectionSet { - [field: string]: BodyType - } - interface iDenormalize { - field: string - body: { - [field: string]: number - } - } - - interface GraphQLQuery { - embody?: (transform: TEmbodyArgs) => void - $filter?: Mongo.Selector - $options?: Mongo.Options - // Integer - maxDepth?: number - // Integer - maxLimit?: number - deny?: string[] - intersect?: GrapherBody - } - - interface Exposure { - firewall?: TFirewall | TFirewall[] - publication?: boolean // Boolean - method?: boolean // Boolean - blocking?: boolean // Boolean - maxLimit?: number // Number - maxDepth?: number // Number - restrictedFields?: string[] // [String] - restrictLinks?: string[] | ((...args: any[]) => any) // [String] or Function, - } - - interface ASTToQueryOptions { - maxLimit: number - maxDepth: number - } - } - - export function setAstToQueryDefaults( - options: Grapher.ASTToQueryOptions - ): void - - export const db: Readonly<{ - [key: string]: Mongo.CollectionStatic - }> - - export class MemoryResultCacher { - constructor({ ttl }: { ttl: number }) - - public fetch( - cacheId: string, - options: { - query: any - countCursor: any - } - ): TResult - - public storeData(cacheId: string, data: T): void - } -} diff --git a/@types/src/mongo/index.d.ts b/@types/src/mongo/index.d.ts deleted file mode 100644 index f5514d46..00000000 --- a/@types/src/mongo/index.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Grapher } from 'meteor/cultofcoders:grapher' -import { Mongo } from 'meteor/mongo' -import { DocumentNode } from 'graphql' - -declare module 'meteor/mongo' { - module Mongo { - interface Options {} - - interface Collection { - attachSchema(schema: T): void - astToQuery( - ast: DocumentNode, - query: Grapher.GraphQLQuery - ): Mongo.Cursor - createQuery( - name: string, - body: Grapher.Body | {}, - options?: {} - ): Grapher.Query - createQuery( - body: Grapher.Body | {}, - options?: {} - ): Grapher.Query - expose: Grapher.Exposure - addLinks(links: Grapher.Link): void - addReducers(): void - } - } -} diff --git a/MIGRATION.md b/MIGRATION.md index d3ae7acf..f476f9bd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,11 @@ ## Migrations +### From 1.x -> 2.0 (Meteor 3.0) + +- Static queries on frontend must use `query.fetchAsync()` instead of `query.fetch(cb)`. `query.fetch()` worked with reactive and static queries before, but now we have to use `Meteor.callAsync()` instead of `Meteor.call()` so underlying `_fetchStatic` method returns a Promise for static queries. + +- Server-side queries should use `fetchAsync`, `fetchOneAsync` and `countAsync` instead of their synchronous counterparts. + ### From 1.3.5 -> 1.3.6 When you use reducers with a body that uses a link that should return a single result, you will now get the object, not an array with a single element. @@ -12,11 +18,11 @@ For example the following link: ```js Users.addLinks({ - post: { - type: 'one', - collection: Posts, - field: 'postId' - } + post: { + type: 'one', + collection: Posts, + field: 'postId', + }, }); ``` @@ -25,11 +31,11 @@ Requires the respective field in your Collection's schema: ```js // schema for Users SimpleSchema({ - postId: { - type: String, - optional: true - } -}) + postId: { + type: String, + optional: true, + }, +}); ``` The `metadata` link configuration is no longer an object, but a `Boolean` @@ -37,12 +43,12 @@ The `metadata` link configuration is no longer an object, but a `Boolean` ```js // no longer working Users.addLinks({ - profile: { - collection: Profiles, - metadata: { - createdAt: { type: Date }, - }, + profile: { + collection: Profiles, + metadata: { + createdAt: { type: Date }, }, + }, }); ``` @@ -51,10 +57,10 @@ Users.addLinks({ ```js // working Users.addLinks({ - profile: { - collection: Profiles, - metadata: true, - }, + profile: { + collection: Profiles, + metadata: true, + }, }); ``` diff --git a/Meteor_v3.md b/Meteor_v3.md new file mode 100644 index 00000000..4a8ade68 --- /dev/null +++ b/Meteor_v3.md @@ -0,0 +1,338 @@ +# Steps done + +1. Installed 3.0.0-beta.0 tests and added in api.versionFrom() +2. Removed dependencies that don't support Meteor v3 (denormalize) +3. Fixed dburles:mongo-collection-instances@0.4.0: using v1.0.0 from local packages dir +4. Tried running with peerlibrary:subscription-scope@0.5.0. Not working because of dependencies mismatch. + +### dburles:mongo-collection-instances@0.4.0 + +```bash +MONGO_URL= METEOR_PACKAGE_DIRS="../" TEST_BROWSER_DRIVER=chrome meteor test-packages --once --port 3010 ../ +[[[[[ Tests ]]]]] + +=> Started proxy. +=> Build failed: + + While selecting package versions: + error: Conflict: Constraint mongo@1.0.8 || 1.12.0 || 1.16.0 is not satisfied by mongo 2.0.0-beta300.0. + Constraints on package "mongo": + * mongo@~2.0.0-beta300.0 <- top level + * mongo@2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * mongo@2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * mongo@1.12.0 || 1.16.1 || 2.0.0-beta300.0 <- matb33:collection-hooks 1.3.1 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * mongo@1.12.0 || 1.16.1 || 2.0.0-beta300.0 <- matb33:collection-hooks 1.3.1 <- local-test:cultofcoders:grapher 1.5.0 + * mongo@1.0.8 || 1.12.0 || 1.16.0 <- dburles:mongo-collection-instances 0.4.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * mongo@1.0.8 || 1.12.0 || 1.16.0 <- dburles:mongo-collection-instances 0.4.0 <- local-test:cultofcoders:grapher 1.5.0 + * mongo@1.16.8 || 2.0.0-beta300.0 <- lai:collection-extensions 1.0.0-beta300.1 <- dburles:mongo-collection-instances 0.4.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * mongo@1.16.8 || 2.0.0-beta300.0 <- lai:collection-extensions 1.0.0-beta300.1 <- dburles:mongo-collection-instances 0.4.0 <- + local-test:cultofcoders:grapher 1.5.0 + * mongo@2.0.0-beta300.0 <- tinytest 2.0.0-beta300.0 <- test-in-browser 1.4.0-beta300.0 + * mongo@2.0.0-beta300.0 <- reactive-dict 1.3.2-beta300.0 <- session 1.2.2-beta300.0 <- test-in-browser 1.4.0-beta300.0 + + Conflict: Constraint lai:collection-extensions@0.4.0 is not satisfied by lai:collection-extensions 1.0.0-beta300.1. + Constraints on package "lai:collection-extensions": + * lai:collection-extensions@0.4.0 <- dburles:mongo-collection-instances 0.4.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * lai:collection-extensions@0.4.0 <- dburles:mongo-collection-instances 0.4.0 <- local-test:cultofcoders:grapher 1.5.0 +``` + +Resolved by adding local package pointing to 3.0 migrate branch. + +### peerlibrary:subscription-scope@0.5.0 + +```bash +MONGO_URL= METEOR_PACKAGE_DIRS="../:packages" TEST_BROWSER_DRIVER=chrome meteor test-packages --once --port 3010 ../ + Selecting package versions | + Selecting package versions | + + +[[[[[ Tests ]]]]] + +=> Started proxy. + Selecting package versions \ +=> Build failed: + + While selecting package versions: + error: Conflict: Constraint peerlibrary:subscription-scope@0.5.0 is not satisfied by peerlibrary:subscription-scope 0.1.0. + Constraints on package "peerlibrary:subscription-scope": + * peerlibrary:subscription-scope@0.5.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + + Conflict: Constraint minimongo@1.0.6 is not satisfied by minimongo 2.0.0-beta300.0. + Constraints on package "minimongo": + * minimongo@~2.0.0-beta300.0 <- top level + * minimongo@2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * minimongo@2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * minimongo@2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * minimongo@2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * minimongo@1.7.0 || 1.9.0 || 2.0.0-beta300.0 <- matb33:collection-hooks 1.3.1 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * minimongo@1.7.0 || 1.9.0 || 2.0.0-beta300.0 <- matb33:collection-hooks 1.3.1 <- local-test:cultofcoders:grapher 1.5.0 + * minimongo@1.0.6 <- peerlibrary:subscription-scope 0.1.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher + 1.5.0 + + Conflict: Constraint meteor@1.1.5 is not satisfied by meteor 2.0.0-beta300.0. + Constraints on package "meteor": + * meteor@~2.0.0-beta300.0 <- top level + * meteor@2.0.0-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- core-runtime 1.0.0-beta300.0 <- meteor 2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo + 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- core-runtime 1.0.0-beta300.0 <- meteor 2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo + 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- core-runtime 1.0.0-beta300.0 <- meteor 2.0.0-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- webapp 2.0.0-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- react-fast-refresh 0.2.8-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 + <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- react-fast-refresh 0.2.8-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 + <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- react-fast-refresh 0.2.8-beta300.0 <- ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- modules 0.19.1-beta300.0 <- babel-runtime 1.5.2-beta300.0 <- ecmascript 0.16.8-beta300.0 <- + allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- modules 0.19.1-beta300.0 <- babel-runtime 1.5.2-beta300.0 <- ecmascript 0.16.8-beta300.0 <- + allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- modules 0.19.1-beta300.0 <- babel-runtime 1.5.2-beta300.0 <- ecmascript 0.16.8-beta300.0 <- + autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- modules 0.19.1-beta300.0 <- ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- modules-runtime 0.13.2-beta300.0 <- modules 0.19.1-beta300.0 <- babel-runtime 1.5.2-beta300.0 <- + ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- modules-runtime 0.13.2-beta300.0 <- modules 0.19.1-beta300.0 <- babel-runtime 1.5.2-beta300.0 <- + ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- modules-runtime 0.13.2-beta300.0 <- modules 0.19.1-beta300.0 <- babel-runtime 1.5.2-beta300.0 <- + ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- modules-runtime 0.13.2-beta300.0 <- modules 0.19.1-beta300.0 <- ecmascript 0.16.8-beta300.0 <- + autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- babel-compiler 7.11.0-beta300.0 <- ecmascript + 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- babel-compiler 7.11.0-beta300.0 <- ecmascript + 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- babel-compiler 7.11.0-beta300.0 <- ecmascript + 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime-client 0.12.2-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- + babel-compiler 7.11.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime-client 0.12.2-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- + babel-compiler 7.11.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime-client 0.12.2-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- + babel-compiler 7.11.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime-client 0.12.2-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- ecmascript + 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- promise 1.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- modern-browsers 0.1.10-beta300.0 <- babel-compiler 7.11.0-beta300.0 <- ecmascript + 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- modern-browsers 0.1.10-beta300.0 <- babel-compiler 7.11.0-beta300.0 <- ecmascript + 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- modern-browsers 0.1.10-beta300.0 <- babel-compiler 7.11.0-beta300.0 <- ecmascript + 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- modern-browsers 0.1.10-beta300.0 <- webapp 2.0.0-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime-server 0.11.1-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- + babel-compiler 7.11.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime-server 0.11.1-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- + babel-compiler 7.11.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime-server 0.11.1-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- + babel-compiler 7.11.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ecmascript-runtime-server 0.11.1-beta300.0 <- ecmascript-runtime 0.8.2-beta300.0 <- ecmascript + 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- babel-runtime 1.5.2-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- + mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- babel-runtime 1.5.2-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- + mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- babel-runtime 1.5.2-beta300.0 <- ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- dynamic-import 0.7.4-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- + mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- dynamic-import 0.7.4-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- + mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- dynamic-import 0.7.4-beta300.0 <- ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- fetch 0.1.4-beta300.0 <- dynamic-import 0.7.4-beta300.0 <- ecmascript 0.16.8-beta300.0 <- + allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- fetch 0.1.4-beta300.0 <- dynamic-import 0.7.4-beta300.0 <- ecmascript 0.16.8-beta300.0 <- + allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- fetch 0.1.4-beta300.0 <- dynamic-import 0.7.4-beta300.0 <- ecmascript 0.16.8-beta300.0 <- + autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- inter-process-messaging 0.1.2-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- babel-compiler 7.11.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- + mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- babel-compiler 7.11.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- allow-deny 2.0.0-beta300.0 <- + mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- babel-compiler 7.11.0-beta300.0 <- ecmascript 0.16.8-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- logging 1.3.3-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- logging 1.3.3-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ejson 1.1.4-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ejson 1.1.4-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ejson 1.1.4-beta300.0 <- check 1.3.3-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- base64 1.0.13-beta300.0 <- ejson 1.1.4-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo + 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- base64 1.0.13-beta300.0 <- ejson 1.1.4-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo + 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- base64 1.0.13-beta300.0 <- ejson 1.1.4-beta300.0 <- check 1.3.3-beta300.0 <- autoupdate + 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- typescript 4.9.5-beta300.0 <- logging 1.3.3-beta300.0 <- mongo 2.0.0-beta300.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- typescript 4.9.5-beta300.0 <- logging 1.3.3-beta300.0 <- mongo 2.0.0-beta300.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- underscore 1.0.14-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher + 1.5.0 + * meteor@2.0.0-beta300.0 <- underscore 1.0.14-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- routepolicy 1.1.2-beta300.0 <- ddp-server 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- routepolicy 1.1.2-beta300.0 <- ddp-server 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- routepolicy 1.1.2-beta300.0 <- ddp-server 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate + 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- routepolicy 1.1.2-beta300.0 <- webapp 2.0.0-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- boilerplate-generator 2.0.0-beta300.0 <- webapp 2.0.0-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- webapp-hashing 1.1.2-beta300.0 <- webapp 2.0.0-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- callback-hook 1.6.0-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- callback-hook 1.6.0-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- callback-hook 1.6.0-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate + 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- callback-hook 1.6.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- check 1.3.3-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- check 1.3.3-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- check 1.3.3-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo + 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo + 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- random 1.2.2-beta300.0 <- caching-compiler 2.0.0-beta300.0 <- caching-html-compiler + 2.0.0-alpha300.17 <- templating-compiler 2.0.0-alpha300.17 <- templating 1.4.4-alpha300.17 <- test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- random 1.2.2-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate + 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- random 1.2.2-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- tracker 1.3.3-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- retry 1.1.1-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- id-map 1.2.0-beta300.0 <- binary-heap 1.0.12-beta300.0 <- mongo 2.0.0-beta300.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- id-map 1.2.0-beta300.0 <- binary-heap 1.0.12-beta300.0 <- mongo 2.0.0-beta300.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp-common 1.4.1-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp-common 1.4.1-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp-common 1.4.1-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate + 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- reload 1.3.2-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- socket-stream-client 0.5.2-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- + allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- socket-stream-client 0.5.2-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- + allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- socket-stream-client 0.5.2-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- + autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- diff-sequence 1.1.3-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- diff-sequence 1.1.3-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- diff-sequence 1.1.3-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate + 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- diff-sequence 1.1.3-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- mongo-id 1.0.9-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- mongo-id 1.0.9-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- mongo-id 1.0.9-beta300.0 <- ddp-client 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate + 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- mongo-id 1.0.9-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp-server 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo + 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp-server 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo + 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ddp-server 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- facts-base 1.0.2-beta300.0 <- ddp-server 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- facts-base 1.0.2-beta300.0 <- ddp-server 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- allow-deny + 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- facts-base 1.0.2-beta300.0 <- ddp-server 3.0.0-beta300.0 <- ddp 1.4.2-beta300.0 <- autoupdate + 2.0.0-beta300.0 + * meteor@2.0.0-beta300.0 <- facts-base 1.0.2-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- reactive-var 1.0.13-beta300.0 <- blaze 3.0.0-alpha300.17 <- spacebars 2.0.0-alpha300.17 <- + templating-runtime 2.0.0-alpha300.17 <- templating 1.4.4-alpha300.17 <- test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- reactive-var 1.0.13-beta300.0 <- blaze 3.0.0-alpha300.17 <- spacebars 2.0.0-alpha300.17 <- + test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- reactive-var 1.0.13-beta300.0 <- blaze 3.0.0-alpha300.17 <- test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- npm-mongo 4.16.1-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- npm-mongo 4.16.1-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- minimongo 2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- minimongo 2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- mongo 2.0.0-beta300.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- minimongo 2.0.0-beta300.0 <- matb33:collection-hooks 1.3.1 <- local-test:cultofcoders:grapher + 1.5.0 + * meteor@2.0.0-beta300.0 <- geojson-utils 1.0.12-beta300.0 <- minimongo 2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- + mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- geojson-utils 1.0.12-beta300.0 <- minimongo 2.0.0-beta300.0 <- allow-deny 2.0.0-beta300.0 <- + mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- geojson-utils 1.0.12-beta300.0 <- minimongo 2.0.0-beta300.0 <- matb33:collection-hooks 1.3.1 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- ordered-dict 1.2.0-beta300.0 <- blaze 3.0.0-alpha300.17 <- spacebars 2.0.0-alpha300.17 <- + templating-runtime 2.0.0-alpha300.17 <- templating 1.4.4-alpha300.17 <- test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ordered-dict 1.2.0-beta300.0 <- blaze 3.0.0-alpha300.17 <- spacebars 2.0.0-alpha300.17 <- + test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- ordered-dict 1.2.0-beta300.0 <- blaze 3.0.0-alpha300.17 <- test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- mongo-dev-server 1.1.1-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- mongo-dev-server 1.1.1-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher + 1.5.0 + * meteor@2.0.0-beta300.0 <- binary-heap 1.0.12-beta300.0 <- mongo 2.0.0-beta300.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- binary-heap 1.0.12-beta300.0 <- mongo 2.0.0-beta300.0 <- local-test:cultofcoders:grapher 1.5.0 + * meteor@1.1.5 <- coffeescript 1.0.6 <- peerlibrary:subscription-scope 0.1.0 <- cultofcoders:grapher 1.5.0 <- + local-test:cultofcoders:grapher 1.5.0 + * meteor@2.0.0-beta300.0 <- caching-compiler 2.0.0-beta300.0 <- caching-html-compiler 2.0.0-alpha300.17 <- + templating-compiler 2.0.0-alpha300.17 <- templating 1.4.4-alpha300.17 <- test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- tinytest 2.0.0-beta300.0 <- test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- session 1.2.2-beta300.0 <- test-in-browser 1.4.0-beta300.0 + * meteor@2.0.0-beta300.0 <- reactive-dict 1.3.2-beta300.0 <- session 1.2.2-beta300.0 <- test-in-browser 1.4.0-beta300.0 +``` + +Fix #1: cloned into local packages and updated minimongo to Meteor v3 + +```bash +MONGO_URL= METEOR_PACKAGE_DIRS="../:packages" TEST_BROWSER_DRIVER=chrome meteor test-packages --once --port 3010 ../ +[[[[[ Tests ]]]]] + +=> Started proxy. +=> Build failed: + + While selecting package versions: + error: Conflict: Constraint coffeescript@2.4.1 is not satisfied by coffeescript 1.0.1. + Constraints on package "coffeescript": + * coffeescript@2.4.1 <- peerlibrary:subscription-scope 0.5.0 <- cultofcoders:grapher 1.5.0 <- local-test:cultofcoders:grapher + 1.5.0 + * coffeescript@2.4.1 <- peerlibrary:extend-publish 0.6.0 <- peerlibrary:subscription-scope 0.5.0 <- cultofcoders:grapher + 1.5.0 <- local-test:cultofcoders:grapher 1.5.0 +``` diff --git a/README.md b/README.md index 63991dc8..9b1043e4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - Read more about our approach coming from Meteor: https://www.bluelibs.com/blog/2021/11/26/the-meteor-of-2022 - We've implemented [Grapher aka Nova](https://www.bluelibs.com/products/nova) as a standalone npm package compatible to native MongoDB drivers (including Meteor), it is not as feature-rich (no meta links, no pubsub functionality) but is more advanced. -# Grapher 1.5 +# Grapher 2.0 _Grapher_ is a Data Fetching Layer on top of Meteor and MongoDB. It is production ready and battle tested. Brought to you by [Cult of Coders](https://www.cultofcoders.com) — Web & Mobile Development Company. diff --git a/docs/introduction.md b/docs/introduction.md index 7f03e221..0d7b0517 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -5,16 +5,19 @@ Grapher is composed of 3 main modules, that work together: #### Link Manager + This module allows you to configure relationships between collections and allows you to create denormalized links. #### Query + The query module is used for fetching your data in a friendly manner, such as: + ```js createQuery({ - users: { - firstName: 1 - } -}) + users: { + firstName: 1, + }, +}); ``` It abstracts your query into a graph composed of Collection Nodes and Field Nodes. @@ -24,8 +27,7 @@ it uses the **Hypernova Module** the crown jewl of Grapher, which heavily minimi #### Exposure The exposure represents the layer between your queries and the client, allowing you to securely expose your queries, -only to users that have access. - +only to users that have access. ## Let's begin! @@ -35,32 +37,35 @@ You can use Grapher without defining any links, for example, let's say you have const Posts = new Mongo.Collection('posts'); Meteor.methods({ - posts() { - return Posts.find({}, { - fields: { - title: 1, - createdAt: 1, - createdBy: 1, - } - }).fetch(); - } -}) + posts() { + return Posts.find( + {}, + { + fields: { + title: 1, + createdAt: 1, + createdBy: 1, + }, + }, + ).fetch(); + }, +}); ``` Transforming this into a Grapher query looks like this: ```js Meteor.methods({ - posts() { - const query = Posts.createQuery({ - title: 1, - createdAt: 1, - createdBy: 1, - }); - - return query.fetch(); - } -}) + posts() { + const query = Posts.createQuery({ + title: 1, + createdAt: 1, + createdBy: 1, + }); + + return query.fetch(); + }, +}); ``` One of the advantages that Grapher has, is the fact that it forces you to specify the fields you need, @@ -72,23 +77,23 @@ If, for example, you want to filter or sort your query, we introduce the `$filte ```js Meteor.methods({ - posts() { - // Previously Posts.find({isApproved: true}, {sort: '...', fields: '...'}); - const query = Posts.createQuery({ - $filters: { - isApproved: true, - }, - $options: { - sort: {createdAt: -1} - }, - title: 1, - createdAt: 1, - createdBy: 1, - }); - - return query.fetch(); - } -}) + posts() { + // Previously Posts.find({isApproved: true}, {sort: '...', fields: '...'}); + const query = Posts.createQuery({ + $filters: { + isApproved: true, + }, + $options: { + sort: { createdAt: -1 }, + }, + title: 1, + createdAt: 1, + createdBy: 1, + }); + + return query.fetch(); + }, +}); ``` If for example you are searching an element by `_id`, you may have `$filters: {_id: 'XXX'}`, then instead of `fetch()` you @@ -105,13 +110,13 @@ which allows the query to receive parameters and adapt before it executes: // We export the query, notice there is no .fetch() export default Posts.createQuery({ - $filter({filters, options, params}) { + $filter({ filters, options, params }) { filters.isApproved = params.isApproved; - }, - $options: {sort: {createdAt: -1}}, - title: 1, - createdAt: 1, - createdBy: 1, + }, + $options: { sort: { createdAt: -1 } }, + title: 1, + createdAt: 1, + createdBy: 1, }); ``` @@ -127,12 +132,14 @@ Lets see how we can re-use the query defined above: import postListQuery from '...'; Meteor.methods({ - posts() { - return postListQuery.clone({ - isApproved: true - }).fetch() - } -}) + posts() { + return postListQuery + .clone({ + isApproved: true, + }) + .fetch(); + }, +}); ``` Whenever we want to use a modular query, we have to `clone()` it so it creates a new instance of it. @@ -144,18 +151,18 @@ You can also use `setParams()` to configure parameters, which extends the curren import postListQuery from '...'; Meteor.methods({ - posts() { - const query = postListQuery.clone(); - - // Warning, if you don't use .clone() and you just .setParams(), - // those params will remain stored in your query - query.setParams({ - isApproved: true, - }); - - return query.fetch(); - } -}) + posts() { + const query = postListQuery.clone(); + + // Warning, if you don't use .clone() and you just .setParams(), + // those params will remain stored in your query + query.setParams({ + isApproved: true, + }); + + return query.fetch(); + }, +}); ``` ## Validating Params @@ -197,6 +204,7 @@ If you want to craft your own validation, it also accepts a function that takes Note: params validation is done prior to fetching the query, not when you do `setParams()` or `clone()` If you want to store some default parameters, you can use the `params` option: + ```js export default Posts.createQuery({...}, { params: { diff --git a/lib/createQuery.js b/lib/createQuery.js index 0f55d7a8..cec89417 100644 --- a/lib/createQuery.js +++ b/lib/createQuery.js @@ -1,67 +1,86 @@ import Query from './query/query.js'; import NamedQuery from './namedQuery/namedQuery.js'; import NamedQueryStore from './namedQuery/store.js'; +import { _ } from 'meteor/underscore'; /** * This is a polymorphic function, it allows you to create a query as an object * or it also allows you to re-use an existing query if it's a named one * - * @param args + * @param {[string, import('meteor/cultofcoders:grapher').Body, import('meteor/cultofcoders:grapher').QueryOptions] | [import('meteor/cultofcoders:grapher').Body, import('meteor/cultofcoders:grapher').QueryOptions]} args * @returns {*} */ export default (...args) => { - if (typeof args[0] === 'string') { - let [name, body, options] = args; - options = options || {}; + if (typeof args[0] === 'string') { + let [name, body, options] = args; + options = options || {}; - // It's a resolver query - if (_.isFunction(body)) { - return createNamedQuery(name, null, body, options); - } + // It's a resolver query + if (_.isFunction(body)) { + return createNamedQuery(name, null, body, options); + } - const entryPointName = _.first(_.keys(body)); - const collection = Mongo.Collection.get(entryPointName); + const entryPointName = _.first(_.keys(body)); + const collection = Mongo.Collection.get(entryPointName); - if (!collection) { - throw new Meteor.Error('invalid-name', `We could not find any collection with the name "${entryPointName}". Make sure it is imported prior to using this`) - } + if (!collection) { + throw new Meteor.Error( + 'invalid-name', + `We could not find any collection with the name "${entryPointName}". Make sure it is imported prior to using this`, + ); + } - return createNamedQuery(name, collection, body[entryPointName], options); - } else { - // Query Creation, it can have an endpoint as collection or as a NamedQuery - let [body, options] = args; - options = options || {}; + return createNamedQuery(name, collection, body[entryPointName], options); + } else { + // Query Creation, it can have an endpoint as collection or as a NamedQuery + let [body, options] = args; + options = options || {}; - const entryPointName = _.first(_.keys(body)); - const collection = Mongo.Collection.get(entryPointName); + const entryPointName = _.first(_.keys(body)); + const collection = Mongo.Collection.get(entryPointName); - if (!collection) { - if (Meteor.isDevelopment && !NamedQueryStore.get(entryPointName)) { - console.warn(`You are creating a query with the entry point "${entryPointName}", but there was no collection found for it (maybe you forgot to import it client-side?). It's assumed that it's referencing a NamedQuery.`) - } + if (!collection) { + if (Meteor.isDevelopment && !NamedQueryStore.get(entryPointName)) { + console.warn( + `You are creating a query with the entry point "${entryPointName}", but there was no collection found for it (maybe you forgot to import it client-side?). It's assumed that it's referencing a NamedQuery.`, + ); + } - return createNamedQuery(entryPointName, null, {}, {params: body[entryPointName]}); - } else { - return createNormalQuery(collection, body[entryPointName], options); - } + return createNamedQuery( + entryPointName, + null, + {}, + { params: body[entryPointName] }, + ); + } else { + return createNormalQuery(collection, body[entryPointName], options); } -} + } +}; +/** + * + * @param {string} name + * @param {Mongo.Collection | null} collection + * @param {import('meteor/cultofcoders:grapher').Body} body + * @param {import('meteor/cultofcoders:grapher').CreateQueryOptions} options + * @returns + */ function createNamedQuery(name, collection, body, options = {}) { - // if it exists already, we re-use it - const namedQuery = NamedQueryStore.get(name); - let query; + // if it exists already, we re-use it + const namedQuery = NamedQueryStore.get(name); + let query; - if (!namedQuery) { - query = new NamedQuery(name, collection, body, options); - NamedQueryStore.add(name, query); - } else { - query = namedQuery.clone(options.params); - } + if (!namedQuery) { + query = new NamedQuery(name, collection, body, options); + NamedQueryStore.add(name, query); + } else { + query = namedQuery.clone(options.params); + } - return query; + return query; } -function createNormalQuery(collection, body, options) { - return new Query(collection, body, options); +function createNormalQuery(collection, body, options) { + return new Query(collection, body, options); } diff --git a/lib/exposure/exposure.js b/lib/exposure/exposure.js index 0239be25..c649a852 100755 --- a/lib/exposure/exposure.js +++ b/lib/exposure/exposure.js @@ -1,11 +1,12 @@ +import { publishComposite } from 'meteor/reywood:publish-composite'; import genCountEndpoint from '../query/counts/genEndpoint.server.js'; import createGraph from '../query/lib/createGraph.js'; import recursiveCompose from '../query/lib/recursiveCompose.js'; import hypernova from '../query/hypernova/hypernova.js'; import { - ExposureSchema, - ExposureDefaults, - validateBody, + ExposureSchema, + ExposureDefaults, + validateBody, } from './exposure.config.schema.js'; import enforceMaxDepth from './lib/enforceMaxDepth.js'; import enforceMaxLimit from './lib/enforceMaxLimit.js'; @@ -14,299 +15,353 @@ import deepClone from 'lodash.clonedeep'; import restrictFieldsFn from './lib/restrictFields.js'; import restrictLinks from './lib/restrictLinks.js'; import { check } from 'meteor/check'; +import { _ } from 'meteor/underscore'; let globalConfig = {}; export default class Exposure { - static setConfig(config) { - Object.assign(globalConfig, config); + /** + * + * @param {Grapher.ExposureConfig} config + * @returns {void} + */ + static setConfig(config) { + Object.assign(globalConfig, config); + } + + /** + * + * @returns {Grapher.ExposureConfig} + */ + static getConfig() { + return globalConfig; + } + + /** + * + * @param {Grapher.Filters} filters + * @param {Grapher.Options} options + * @param {string[]} restrictedFields + */ + static restrictFields(filters, options, restrictedFields) { + return restrictFieldsFn(filters, options, restrictedFields); + } + + /** + * + * @param {Mongo.Collection} collection + * @param {Grapher.ExposureConfig} config + */ + constructor(collection, config = {}) { + collection.__isExposedForGrapher = true; + collection.__exposure = this; + + this.collection = collection; + this.name = `exposure_${collection._name}`; + + this.config = config; + this._validateAndClean(); + + this.initSecurity(); + + if (this.config.publication) { + this.initPublication(); } - static getConfig() { - return globalConfig; + if (this.config.method) { + this.initMethod(); } - static restrictFields(...args) { - return restrictFieldsFn(...args); + if (!this.config.method && !this.config.publication) { + throw new Meteor.Error( + 'weird', + 'If you want to expose your collection you need to specify at least one of ["method", "publication"] options to true', + ); } - constructor(collection, config = {}) { - collection.__isExposedForGrapher = true; - collection.__exposure = this; + this.initCountMethod(); + this.initCountPublication(); + } - this.collection = collection; - this.name = `exposure_${collection._name}`; - - this.config = config; - this._validateAndClean(); - - this.initSecurity(); - - if (this.config.publication) { - this.initPublication(); - } - - if (this.config.method) { - this.initMethod(); - } - - if (!this.config.method && !this.config.publication) { - throw new Meteor.Error( - 'weird', - 'If you want to expose your collection you need to specify at least one of ["method", "publication"] options to true' - ); - } - - this.initCountMethod(); - this.initCountPublication(); + _validateAndClean() { + if (typeof this.config === 'function') { + const firewall = this.config; + this.config = { firewall }; } - _validateAndClean() { - if (typeof this.config === 'function') { - const firewall = this.config; - this.config = { firewall }; - } + this.config = Object.assign( + {}, + ExposureDefaults, + Exposure.getConfig(), + this.config, + ); + check(this.config, ExposureSchema); - this.config = Object.assign( - {}, - ExposureDefaults, - Exposure.getConfig(), - this.config - ); - check(this.config, ExposureSchema); - - if (this.config.body) { - validateBody(this.collection, this.config.body); - } + if (this.config.body) { + validateBody(this.collection, this.config.body); } - - /** - * Takes the body and intersects it with the exposure body, if it exists. - * - * @param body - * @param userId - * @returns {*} - */ - getTransformedBody(body, userId) { - if (!this.config.body) { - return body; - } - - const processedBody = this.getBody(userId); - - if (processedBody === true) { - return body; - } - - return cleanBody(processedBody, body); + } + + /** + * Takes the body and intersects it with the exposure body, if it exists. + * + * @param {Grapher.Body} body + * @param {string} [userId] + * @returns {*} + */ + getTransformedBody(body, userId) { + if (!this.config.body) { + return body; } - /** - * Gets the exposure body - */ - getBody(userId) { - if (!this.config.body) { - throw new Meteor.Error( - 'missing-body', - 'Cannot get exposure body because it was not defined.' - ); - } - - let body; - if (_.isFunction(this.config.body)) { - body = this.config.body.call(this, userId); - } else { - body = this.config.body; - } - - // it means we allow everything, no need for cloning. - if (body === true) { - return true; - } + const processedBody = this.getBody(userId); - return deepClone(body, userId); + if (processedBody === true) { + return body; } - /** - * Initializing the publication for reactive query fetching - */ - initPublication() { - const collection = this.collection; - const config = this.config; - const getTransformedBody = this.getTransformedBody.bind(this); - - Meteor.publishComposite(this.name, function(body) { - if (!config.blocking) { - this.unblock(); - } + return cleanBody(processedBody, body); + } + + /** + * Gets the exposure body + * @param {string} [userId] + */ + getBody(userId) { + if (!this.config.body) { + throw new Meteor.Error( + 'missing-body', + 'Cannot get exposure body because it was not defined.', + ); + } - let transformedBody = getTransformedBody(body); + let body; + if (_.isFunction(this.config.body)) { + body = this.config.body.call(this, userId); + } else { + body = this.config.body; + } - const rootNode = createGraph(collection, transformedBody); + // it means we allow everything, no need for cloning. + if (body === true) { + return true; + } - enforceMaxDepth(rootNode, config.maxDepth); - restrictLinks(rootNode, this.userId); + return deepClone(body, userId); + } - return recursiveCompose(rootNode, this.userId, { - bypassFirewalls: !!config.body, - blocking: config.blocking, - }); - }); - } + /** + * Initializing the publication for reactive query fetching + */ + initPublication() { + const collection = this.collection; + const config = this.config; + const getTransformedBody = this.getTransformedBody.bind(this); /** - * Initializez the method to retrieve the data via Meteor.call + * @this {Meteor.MethodThisType} */ - initMethod() { - const collection = this.collection; - const config = this.config; - const getTransformedBody = this.getTransformedBody.bind(this); - - const methodBody = function(body) { - if (!config.blocking) { - this.unblock(); - } + publishComposite(this.name, function (body) { + if (!config.blocking) { + this.unblock(); + } - let transformedBody = getTransformedBody(body); + let transformedBody = getTransformedBody(body); - const rootNode = createGraph(collection, transformedBody); + const rootNode = createGraph(collection, transformedBody); - enforceMaxDepth(rootNode, config.maxDepth); - restrictLinks(rootNode, this.userId); + enforceMaxDepth(rootNode, config.maxDepth); + restrictLinks(rootNode, this.userId); - // if there is no exposure body defined, then we need to apply firewalls - return hypernova(rootNode, this.userId, { - bypassFirewalls: !!config.body, - }); - }; - - Meteor.methods({ - [this.name]: methodBody, - }); - } + return recursiveCompose(rootNode, this.userId, { + bypassFirewalls: !!config.body, + blocking: config.blocking, + }); + }); + } - /** - * Initializes the method to retrieve the count of the data via Meteor.call - * @returns {*} - */ - initCountMethod() { - const collection = this.collection; - - Meteor.methods({ - [this.name + '.count'](body) { - this.unblock(); - - return collection - .find(body.$filters || {}, {}, this.userId, false) - .count(); - }, - }); - } + /** + * Initializes the method to retrieve the data via Meteor.call + */ + initMethod() { + const collection = this.collection; + const config = this.config; + const getTransformedBody = this.getTransformedBody.bind(this); /** - * Initializes the reactive endpoint to retrieve the count of the data. - */ - initCountPublication() { - const collection = this.collection; - - genCountEndpoint(this.name, { - getCursor({ session }) { - return collection.find( - session.filters, - { - fields: { _id: 1 }, - }, - this.userId - ); - }, - - getSession(body) { - return { filters: body.$filters || {} }; - }, - }); - } - - /** - * Initializes security enforcement - * THINK: Maybe instead of overriding .find, I could store this data of security inside the collection object. - */ - initSecurity() { - const collection = this.collection; - const { firewall, maxLimit, restrictedFields } = this.config; - const find = collection.find.bind(collection); - const findOne = collection.findOne.bind(collection); - - collection.firewall = (filters, options, userId) => { - if (userId !== undefined) { - this._callFirewall( - { collection: collection }, - filters, - options, - userId - ); - - enforceMaxLimit(options, maxLimit); - - if (restrictedFields) { - Exposure.restrictFields(filters, options, restrictedFields); - } - } - }; - - collection.find = function(filters, options = {}, userId = undefined, enforceMaxDepth = true) { - if (arguments.length == 0) { - filters = {}; - } - - // If filters is undefined it should return an empty item - if (arguments.length > 0 && filters === undefined) { - return find(undefined, options); - } - - collection.firewall(filters, options, userId); - - if (!enforceMaxDepth) { - delete options.limit; - } - - return find(filters, options); - }; - - collection.findOne = function( - filters, - options = {}, - userId = undefined - ) { - // If filters is undefined it should return an empty item - if (arguments.length > 0 && filters === undefined) { - return null; - } - - if (typeof filters === 'string') { - filters = { _id: filters }; - } - - collection.firewall(filters, options, userId); - - return findOne(filters, options); - }; - } + * @this {Meteor.MethodThisType} + * @param {Grapher.Body} body + * */ + const methodBody = function (body) { + if (!config.blocking) { + this.unblock(); + } + + let transformedBody = getTransformedBody(body); + + const rootNode = createGraph(collection, transformedBody); + + enforceMaxDepth(rootNode, config.maxDepth); + restrictLinks(rootNode, this.userId); + + // if there is no exposure body defined, then we need to apply firewalls + return hypernova(rootNode, this.userId, { + bypassFirewalls: !!config.body, + }); + }; + + Meteor.methods({ + [this.name]: methodBody, + }); + } + + /** + * Initializes the method to retrieve the count of the data via Meteor.call + * @returns {*} + */ + initCountMethod() { + const collection = this.collection; + + Meteor.methods({ + [this.name + '.count'](body) { + return collection + .find(body.$filters || {}, {}, this.userId, false) + .countAsync(); + }, + }); + } + + /** + * Initializes the reactive endpoint to retrieve the count of the data. + */ + initCountPublication() { + const collection = this.collection; + + genCountEndpoint(this.name, { + getCursor({ session }) { + return collection.find( + session.filters, + { + fields: { _id: 1 }, + }, + this.userId, + ); + }, + + getSession(body) { + return { filters: body.$filters || {} }; + }, + }); + } + + /** + * Initializes security enforcement + * THINK: Maybe instead of overriding .find, I could store this data of security inside the collection object. + */ + initSecurity() { + const collection = this.collection; + const { firewall, maxLimit, restrictedFields } = this.config; + const find = collection.find.bind(collection); + const findOneAsync = collection.findOneAsync.bind(collection); + + collection.firewall = async (filters, options, userId) => { + if (userId !== undefined) { + await this._callFirewall( + { collection: collection }, + filters, + options, + userId, + ); - /** - * @private - */ - _callFirewall(...args) { - const { firewall } = this.config; - if (!firewall) { - return; + if (restrictedFields) { + Exposure.restrictFields(filters, options, restrictedFields); } - - if (Array.isArray(firewall)) { - firewall.forEach(fire => { - fire.call(...args); - }); - } else { - firewall.call(...args); + } + }; + + collection.find = function ( + filters, + options = {}, + userId = undefined, + enforceMaxDepth = true, + ) { + if (arguments.length == 0) { + filters = {}; + } + + // If filters is undefined it should return an empty item + if (arguments.length > 0 && filters === undefined) { + return find(undefined, options); + } + + // Before it was in firewall function, but now + // that function is async, it is moved here. + if (userId !== undefined) { + enforceMaxLimit(options, maxLimit); + } + + if (!enforceMaxDepth) { + delete options.limit; + } + + const cursor = find(filters, options); + [ + 'fetchAsync', + 'countAsync', + 'forEachAsync', + 'mapAsync', + 'observe', + // 'observeAsync', + // 'observeChangesAsync', + ].forEach((method) => { + if (cursor[method]) { + const oldCursorMethod = cursor[method]; + cursor[method] = async (...args) => { + await collection.firewall(filters, options, userId); + return oldCursorMethod.call(cursor, ...args); + }; } + }); + + return cursor; + }; + + collection.findOneAsync = async function ( + filters, + options = {}, + userId = undefined, + ) { + // If filters is undefined it should return an empty item + if (arguments.length > 0 && filters === undefined) { + return null; + } + + if (typeof filters === 'string') { + filters = { _id: filters }; + } + + await collection.firewall(filters, options, userId); + + return findOneAsync(filters, options); + }; + } + + /** + * @param {[unknown, ...Parameters]} args + * @private + */ + async _callFirewall(...args) { + const { firewall } = this.config; + if (!firewall) { + return; + } + + if (Array.isArray(firewall)) { + for (const f of firewall) { + await f.call(...args); + } + } else { + await firewall.call(...args); } + } } diff --git a/lib/exposure/extension.js b/lib/exposure/extension.js index b114a30f..1c9d6651 100644 --- a/lib/exposure/extension.js +++ b/lib/exposure/extension.js @@ -1,14 +1,18 @@ import Exposure from './exposure.js'; Object.assign(Mongo.Collection.prototype, { - expose(config) { - if (!Meteor.isServer) { - throw new Meteor.Error( - 'not-allowed', - `You can only expose a collection server side. ${this._name}` - ); - } + /** + * @this {Mongo.Collection} + * @param {Grapher.ExposureConfig} config + */ + expose(config) { + if (!Meteor.isServer) { + throw new Meteor.Error( + 'not-allowed', + `You can only expose a collection server side. ${this._name}`, + ); + } - new Exposure(this, config); - }, + new Exposure(this, config); + }, }); diff --git a/lib/exposure/lib/restrictFields.js b/lib/exposure/lib/restrictFields.js index 8cdc1b7b..943c8e1d 100644 --- a/lib/exposure/lib/restrictFields.js +++ b/lib/exposure/lib/restrictFields.js @@ -1,3 +1,5 @@ +import { _ } from 'meteor/underscore'; + const deepFilterFieldsArray = ['$and', '$or', '$nor']; const deepFilterFieldsObject = ['$not']; @@ -5,92 +7,98 @@ const deepFilterFieldsObject = ['$not']; * This is used to restrict some fields to some users, by passing the fields as array in the exposure object * For example in an user exposure: restrictFields(options, ['services', 'createdAt']) * - * @param filters Object - * @param options Object - * @param restrictedFields Array + * @param {Grapher.Filters} filters + * @param {Grapher.Options} options + * @param {string[]} restrictedFields */ export default function restrictFields(filters, options, restrictedFields) { - if (!Array.isArray(restrictedFields)) { - throw new Meteor.Error('invalid-parameters', 'Please specify an array of restricted fields.'); - } + if (!Array.isArray(restrictedFields)) { + throw new Meteor.Error( + 'invalid-parameters', + 'Please specify an array of restricted fields.', + ); + } - cleanFilters(filters, restrictedFields); - cleanOptions(options, restrictedFields) + cleanFilters(filters, restrictedFields); + cleanOptions(options, restrictedFields); } /** * Deep cleans filters * - * @param filters - * @param restrictedFields + * @param {Grapher.Filters} filters + * @param {string[]} restrictedFields */ function cleanFilters(filters, restrictedFields) { - if (filters) { - cleanObject(filters, restrictedFields); - } + if (filters) { + cleanObject(filters, restrictedFields); + } - deepFilterFieldsArray.forEach(field => { - if (filters[field]) { - filters[field].forEach(element => cleanFilters(element, restrictedFields)); - } - }); + deepFilterFieldsArray.forEach((field) => { + if (filters[field]) { + filters[field].forEach((element) => + cleanFilters(element, restrictedFields), + ); + } + }); - deepFilterFieldsObject.forEach(field => { - if (filters[field]) { - cleanFilters(filters[field], restrictedFields); - } - }); + deepFilterFieldsObject.forEach((field) => { + if (filters[field]) { + cleanFilters(filters[field], restrictedFields); + } + }); } /** * Deeply cleans options * - * @param options - * @param restrictedFields + * @param {Grapher.Options} options + * @param {string[]} restrictedFields */ function cleanOptions(options, restrictedFields) { - if (options.fields) { - cleanObject(options.fields, restrictedFields); + if (options.fields) { + cleanObject(options.fields, restrictedFields); - if (_.keys(options.fields).length === 0) { - _.extend(options.fields, {_id: 1}) - } - } else { - options.fields = {_id: 1}; + if (_.keys(options.fields).length === 0) { + _.extend(options.fields, { _id: 1 }); } + } else { + options.fields = { _id: 1 }; + } - if (options.sort) { - cleanObject(options.sort, restrictedFields); - } + if (options.sort) { + cleanObject(options.sort, restrictedFields); + } } /** * Cleans the object (not deeply) * - * @param object - * @param restrictedFields + * @param {Object.} object + * @param {string[]} restrictedFields + * @returns {void} */ function cleanObject(object, restrictedFields) { - _.each(object, (value, key) => { - restrictedFields.forEach((restrictedField) => { - if (matching(restrictedField, key)) { - delete object[key]; - } - }) + _.each(object, (value, key) => { + restrictedFields.forEach((restrictedField) => { + if (matching(restrictedField, key)) { + delete object[key]; + } }); + }); } /** * Returns true if field == subfield or if `${field}.` INCLUDED in subfield * Example: "profile" and "profile.firstName" will be a matching field - * @param field - * @param subfield + * @param field {string} + * @param subfield {string} * @returns {boolean} */ function matching(field, subfield) { - if (field === subfield) { - return true; - } + if (field === subfield) { + return true; + } - return subfield.slice(0, field.length + 1) === field + '.'; + return subfield.slice(0, field.length + 1) === field + '.'; } diff --git a/lib/exposure/testing/bootstrap/fixtures.js b/lib/exposure/testing/bootstrap/fixtures.js index 7f8f533e..0462455c 100644 --- a/lib/exposure/testing/bootstrap/fixtures.js +++ b/lib/exposure/testing/bootstrap/fixtures.js @@ -1,48 +1,62 @@ import { Exposure } from 'meteor/cultofcoders:grapher'; Exposure.setConfig({ - maxLimit: 5 + maxLimit: 5, }); -import Demo, {DemoPublication, DemoMethod, DemoRestrictedLink} from './demo.js'; +import Demo, { + DemoPublication, + DemoMethod, + DemoRestrictedLink, +} from './demo.js'; import Intersect, { CollectionLink as IntersectLink } from './intersect'; -Demo.remove({}); -DemoRestrictedLink.remove({}); +await Demo.removeAsync({}); +await DemoRestrictedLink.removeAsync({}); -Intersect.remove({}); -IntersectLink.remove({}); +await Intersect.removeAsync({}); +await IntersectLink.removeAsync({}); -Demo.insert({isPrivate: true, restrictedField: 'PRIVATE'}); -Demo.insert({isPrivate: false, restrictedField: 'PRIVATE'}); -Demo.insert({isPrivate: false, restrictedField: 'PRIVATE', date: new Date()}); +await Demo.insertAsync({ isPrivate: true, restrictedField: 'PRIVATE' }); +await Demo.insertAsync({ isPrivate: false, restrictedField: 'PRIVATE' }); +await Demo.insertAsync({ + isPrivate: false, + restrictedField: 'PRIVATE', + date: new Date(), +}); -const restrictedDemoId = Demo.insert({ - isPrivate: false, - restrictedField: 'PRIVATE' +const restrictedDemoId = await Demo.insertAsync({ + isPrivate: false, + restrictedField: 'PRIVATE', }); -Demo.getLink(restrictedDemoId, 'restrictedLink').set({ - test: true +await ( + await Demo.getLink(restrictedDemoId, 'restrictedLink') +).set({ + test: true, }); // INTERSECTION TEST LINKS -const intersectId = Intersect.insert({ - value: 'Hello', - privateValue: 'Bad!' +const intersectId = await Intersect.insertAsync({ + value: 'Hello', + privateValue: 'Bad!', }); -const intersectId2 = Intersect.insert({ - value: 'Goodbye', - privateValue: 'Bad!' +const intersectId2 = await Intersect.insertAsync({ + value: 'Goodbye', + privateValue: 'Bad!', }); -const intersectLinkId = IntersectLink.insert({ - value: 'Hello, I am a Link', - privateValue: 'Bad!' +const intersectLinkId = await IntersectLink.insertAsync({ + value: 'Hello, I am a Link', + privateValue: 'Bad!', }); -Intersect.getLink(intersectId, 'link').set(intersectLinkId); -Intersect.getLink(intersectId, 'privateLink').set(intersectLinkId); -IntersectLink.getLink(intersectLinkId, 'myself').set(intersectLinkId); +await (await Intersect.getLink(intersectId, 'link')).set(intersectLinkId); +await ( + await Intersect.getLink(intersectId, 'privateLink') +).set(intersectLinkId); +await ( + await IntersectLink.getLink(intersectLinkId, 'myself') +).set(intersectLinkId); diff --git a/lib/exposure/testing/client.js b/lib/exposure/testing/client.js index 34afb13d..9df7a6e1 100755 --- a/lib/exposure/testing/client.js +++ b/lib/exposure/testing/client.js @@ -1,217 +1,220 @@ import { assert } from 'chai'; -import Demo, { - DemoMethod, - DemoPublication -} from './bootstrap/demo.js'; +import Demo, { DemoMethod, DemoPublication } from './bootstrap/demo.js'; -import Intersect, { CollectionLink as IntersectLink } from './bootstrap/intersect'; +import Intersect, { + CollectionLink as IntersectLink, +} from './bootstrap/intersect'; +import { _ } from 'meteor/underscore'; describe('Exposure Tests', function () { - it('Should fetch only allowed data and limitations should be applied', function (done) { - const query = Demo.createQuery({ - $options: {limit: 3}, - restrictedField: 1 - }); - - query.fetch((err, res) => { - assert.isUndefined(err); - assert.isDefined(res); - - assert.lengthOf(res, 2); - done(); - }); + it('Should fetch only allowed data and limitations should be applied', async function () { + const query = Demo.createQuery({ + $options: { limit: 4 }, + restrictedField: 1, }); - it('Should not allow me to fetch the graph data, because of maxDepth', function (done) { - const query = Demo.createQuery({ - $options: {limit: 3}, - restrictedField: 1, - children: { - myself: { + const res = await query.fetchAsync(); + assert.isDefined(res); - } - } - }); + assert.lengthOf(res, 2); - query.fetch((err, res) => { - assert.isUndefined(res); - assert.isDefined(err); + // query.fetch((err, res) => { + // console.log('err/res', err, res); + // assert.isUndefined(err); + // assert.isDefined(res); - done(); - }); + // assert.lengthOf(res, 2); + // done(); + // }); + }); + + it('Should not allow me to fetch the graph data, because of maxDepth', function (done) { + const query = Demo.createQuery({ + $options: { limit: 3 }, + restrictedField: 1, + children: { + myself: {}, + }, }); - it('Should return the correct count', function (done) { - Meteor.call('exposure_exposure_test.count', {}, function (err, res) { - assert.isUndefined(err); + query.fetch((err, res) => { + assert.isUndefined(res); + assert.isDefined(err); - assert.equal(3, res); - done(); - }) + done(); }); + }); - it('Should return the correct count via query', function (done) { - const query = Demo.createQuery({ - $options: {limit: 1} - }); - - query.getCount(function (err, res) { - assert.isUndefined(err); + it('Should return the correct count', async function () { + const res = await Meteor.callAsync('exposure_exposure_test.count', {}); + assert.equal(3, res); + }); - assert.equal(3, res); - done(); - }) + it('Should return the correct count via query', function (done) { + const query = Demo.createQuery({ + $options: { limit: 1 }, }); - it('Should return the correct count when querying with filters on date objects', function (done) { - const query = Demo.createQuery({ - $filters: {date: {$lte: new Date()}}, - date: 1 - }); - query.getCount(function (err, res) { - assert.isUndefined(err); + query.getCount(function (err, res) { + assert.isUndefined(err); - assert.equal(1, res); - done(); - }) + assert.equal(3, res); + done(); + }); + }); + it('Should return the correct count when querying with filters on date objects', function (done) { + const query = Demo.createQuery({ + $filters: { date: { $lte: new Date() } }, + date: 1, }); - it('Should should not allow publish but only method', function (done) { - const query = DemoMethod.createQuery({ - _id: 1 - }); - - query.fetch((err, res) => { - assert.isUndefined(err); - assert.isDefined(res); - }); + query.getCount(function (err, res) { + assert.isUndefined(err); - const handler = query.subscribe({ - onStop(e) { - done(); - } - }); + assert.equal(1, res); + done(); }); + }); - it('Should should not allow method but only publish', function (done) { - const query = DemoPublication.createQuery({ - _id: 1 - }); + it('Should should not allow publish but only method', function (done) { + const query = DemoMethod.createQuery({ + _id: 1, + }); - query.fetch((err, res) => { - assert.isDefined(err); - assert.isUndefined(res); - }); + query.fetch((err, res) => { + assert.isUndefined(err); + assert.isDefined(res); + }); - query.subscribe({ - onReady() { - done(); - } - }); + const handler = query.subscribe({ + onStop(e) { + done(); + }, }); + }); + it('Should should not allow method but only publish', function (done) { + const query = DemoPublication.createQuery({ + _id: 1, + }); - it('Should restrict links # restrictLinks ', function (done) { - const query = Demo.createQuery({ - _id: 1, - restrictedLink: {} - }); + query.fetch((err, res) => { + assert.isDefined(err); + assert.isUndefined(res); + }); - query.fetch((err, res) => { - assert.isUndefined(err); + query.subscribe({ + onReady() { + done(); + }, + }); + }); - _.each(res, item => { - assert.isUndefined(item.restrictedLink) - }); + it('Should restrict links # restrictLinks ', function (done) { + const query = Demo.createQuery({ + _id: 1, + restrictedLink: {}, + }); - assert.isArray(res); - assert.isFalse(res.length === 0); + query.fetch((err, res) => { + try { + assert.isUndefined(err); - done(); + _.each(res, (item) => { + assert.isUndefined(item.restrictedLink); }); - }); - it('Should intersect the body graphs - Method', function (done) { - const query = Intersect.createQuery({ - $filters: { - value: 'Hello' - }, - value: 1, - privateValue: 1, - link: { - value: 1, - privateValue: 1, - myself: { - value: 1 - } - }, - privateLink: { - value: 1, - privateValue: 1 - } - }); + assert.isArray(res); + assert.isFalse(res.length === 0); - query.fetch((err, res) => { - assert.isUndefined(err); - assert.lengthOf(res, 1); + done(); + } catch (err) { + done(err); + } + }); + }); + + it('Should intersect the body graphs - Method', function (done) { + const query = Intersect.createQuery({ + $filters: { + value: 'Hello', + }, + value: 1, + privateValue: 1, + link: { + value: 1, + privateValue: 1, + myself: { + value: 1, + }, + }, + privateLink: { + value: 1, + privateValue: 1, + }, + }); - const result = _.first(res); + query.fetch((err, res) => { + assert.isUndefined(err); + assert.lengthOf(res, 1); - assert.isDefined(result.value); - assert.isUndefined(result.privateValue); - assert.isUndefined(result.privateLink); + const result = _.first(res); - assert.isObject(result.link); - assert.isDefined(result.link.value); - assert.isUndefined(result.link.privateValue); - assert.isUndefined(result.link.myself); + assert.isDefined(result.value); + assert.isUndefined(result.privateValue); + assert.isUndefined(result.privateLink); - done(); - }); - }); + assert.isObject(result.link); + assert.isDefined(result.link.value); + assert.isUndefined(result.link.privateValue); + assert.isUndefined(result.link.myself); - it('Should intersect the body graphs - Subscription', function (done) { - const query = Intersect.createQuery({ - $filters: { - value: 'Hello' - }, - value: 1, - privateValue: 1, - link: { - value: 1, - privateValue: 1, - myself: { - value: 1 - } - }, - privateLink: { - value: 1, - privateValue: 1 - } - }); + done(); + }); + }); + + it('Should intersect the body graphs - Subscription', function (done) { + const query = Intersect.createQuery({ + $filters: { + value: 'Hello', + }, + value: 1, + privateValue: 1, + link: { + value: 1, + privateValue: 1, + myself: { + value: 1, + }, + }, + privateLink: { + value: 1, + privateValue: 1, + }, + }); - const handle = query.subscribe(); + const handle = query.subscribe(); - Tracker.autorun((c) => { - if (handle.ready()) { - c.stop(); - const res = query.fetch(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + const res = query.fetch(); - assert.lengthOf(res, 1); + assert.lengthOf(res, 1); - const result = _.first(res); + const result = _.first(res); - assert.isDefined(result.value); - assert.isUndefined(result.privateValue); - assert.isUndefined(result.privateLink); + assert.isDefined(result.value); + assert.isUndefined(result.privateValue); + assert.isUndefined(result.privateLink); - assert.isObject(result.link); - assert.isDefined(result.link.value); - assert.isUndefined(result.link.privateValue); - assert.isUndefined(result.link.myself); + assert.isObject(result.link); + assert.isDefined(result.link.value); + assert.isUndefined(result.link.privateValue); + assert.isUndefined(result.link.myself); - done(); - } - }); - }) + done(); + } + }); + }); }); diff --git a/lib/extension.js b/lib/extension.js index be011614..ed743bd1 100644 --- a/lib/extension.js +++ b/lib/extension.js @@ -1,24 +1,25 @@ import Query from './query/query.js'; import NamedQuery from './namedQuery/namedQuery.js'; import NamedQueryStore from './namedQuery/store.js'; +import { _ } from 'meteor/underscore'; _.extend(Mongo.Collection.prototype, { - createQuery(...args) { - if (args.length === 0) { - return new Query(this, {}, {}); - } - - if (typeof args[0] === 'string') { - //NamedQuery - const [name, body, options] = args; - const query = new NamedQuery(name, this, body, options); - NamedQueryStore.add(name, query); + createQuery(...args) { + if (args.length === 0) { + return new Query(this, {}, {}); + } - return query; - } else { - const [body, options] = args; + if (typeof args[0] === 'string') { + //NamedQuery + const [name, body, options] = args; + const query = new NamedQuery(name, this, body, options); + NamedQueryStore.add(name, query); - return new Query(this, body, options); - } - }, + return query; + } else { + const [body, options] = args; + + return new Query(this, body, options); + } + }, }); diff --git a/lib/links/config.schema.js b/lib/links/config.schema.js index 467afb3f..f355ef3b 100644 --- a/lib/links/config.schema.js +++ b/lib/links/config.schema.js @@ -1,35 +1,38 @@ -import {Match} from 'meteor/check'; -import {Mongo} from 'meteor/mongo'; +import { Match } from 'meteor/check'; +import { Mongo } from 'meteor/mongo'; +import { _ } from 'meteor/underscore'; export const DenormalizeSchema = { - field: String, - body: Object, - bypassSchema: Match.Maybe(Boolean) + field: String, + body: Object, + bypassSchema: Match.Maybe(Boolean), }; +/** + * @type Grapher.LinkConfigDefaults + */ export const LinkConfigDefaults = { - type: 'one', + type: 'one', }; export const LinkConfigSchema = { - type: Match.Maybe(Match.OneOf('one', 'many', '1', '*')), - collection: Match.Maybe( - Match.Where(collection => { - // We do like this so it works with other types of collections - // like FS.Collection - return _.isObject(collection) && ( - collection instanceof Mongo.Collection - || - !!collection._collection - ); - }) - ), - foreignIdentityField: Match.Maybe(String), - field: Match.Maybe(String), - metadata: Match.Maybe(Boolean), - inversedBy: Match.Maybe(String), - index: Match.Maybe(Boolean), - unique: Match.Maybe(Boolean), - autoremove: Match.Maybe(Boolean), - denormalize: Match.Maybe(Match.ObjectIncluding(DenormalizeSchema)), + type: Match.Maybe(Match.OneOf('one', 'many', '1', '*')), + collection: Match.Maybe( + Match.Where((collection) => { + // We do like this so it works with other types of collections + // like FS.Collection + return ( + _.isObject(collection) && + (collection instanceof Mongo.Collection || !!collection._collection) + ); + }), + ), + foreignIdentityField: Match.Maybe(String), + field: Match.Maybe(String), + metadata: Match.Maybe(Boolean), + inversedBy: Match.Maybe(String), + index: Match.Maybe(Boolean), + unique: Match.Maybe(Boolean), + autoremove: Match.Maybe(Boolean), + denormalize: Match.Maybe(Match.ObjectIncluding(DenormalizeSchema)), }; diff --git a/lib/links/extension.js b/lib/links/extension.js index fbc4b9d7..2cdf54e1 100644 --- a/lib/links/extension.js +++ b/lib/links/extension.js @@ -1,88 +1,109 @@ -import { Mongo } from 'meteor/mongo'; import { LINK_STORAGE } from './constants.js'; import Linker from './linker.js'; +import { _ } from 'meteor/underscore'; Object.assign(Mongo.Collection.prototype, { - /** - * The data we add should be valid for config.schema.js - */ - addLinks(data) { - if (!this[LINK_STORAGE]) { - this[LINK_STORAGE] = {}; - } + /** + * The data we add should be valid for config.schema.js + * + * @this {Mongo.Collection} + * @param {Object.} data + * + */ + addLinks(data) { + if (!this[LINK_STORAGE]) { + this[LINK_STORAGE] = {}; + } - _.each(data, (linkConfig, linkName) => { - if (this[LINK_STORAGE][linkName]) { - throw new Meteor.Error( - `You cannot add the link with name: ${linkName} because it was already added to ${ - this._name - } collection` - ); - } + _.each(data, (linkConfig, linkName) => { + if (this[LINK_STORAGE][linkName]) { + throw new Meteor.Error( + `You cannot add the link with name: ${linkName} because it was already added to ${this._name} collection`, + ); + } - const linker = new Linker(this, linkName, linkConfig); + const linker = new Linker(this, linkName, linkConfig); - _.extend(this[LINK_STORAGE], { - [linkName]: linker, - }); - }); - }, + _.extend(this[LINK_STORAGE], { + [linkName]: linker, + }); + }); + }, - getLinks() { - return this[LINK_STORAGE]; - }, + /** + * @this {Mongo.Collection} + * @returns + */ + getLinks() { + return this[LINK_STORAGE]; + }, - getLinker(name) { - if (this[LINK_STORAGE]) { - return this[LINK_STORAGE][name]; - } - }, + /** + * @this {Mongo.Collection} + * @param {string} name + * @returns + */ + getLinker(name) { + if (this[LINK_STORAGE]) { + return this[LINK_STORAGE][name]; + } + }, - hasLink(name) { - if (!this[LINK_STORAGE]) { - return false; - } + /** + * @this {Mongo.Collection} + * @param {string} name + * @returns {boolean} + */ + hasLink(name) { + if (!this[LINK_STORAGE]) { + return false; + } - return !!this[LINK_STORAGE][name]; - }, + return !!this[LINK_STORAGE][name]; + }, - getLink(objectOrId, name) { - let linkData = this[LINK_STORAGE]; + /** + * + * @this {Mongo.Collection} + * @param {unknown} objectOrId + * @param {string} name + * @returns + */ + async getLink(objectOrId, name) { + let linkData = this[LINK_STORAGE]; - if (!linkData) { - throw new Meteor.Error( - `There are no links defined for collection: ${this._name}` - ); - } + if (!linkData) { + throw new Meteor.Error( + `There are no links defined for collection: ${this._name}`, + ); + } - if (!linkData[name]) { - throw new Meteor.Error( - `There is no link ${name} for collection: ${this._name}` - ); - } + if (!linkData[name]) { + throw new Meteor.Error( + `There is no link ${name} for collection: ${this._name}`, + ); + } - const linker = linkData[name]; - let object = objectOrId; - if (typeof objectOrId == 'string') { - if (!linker.isVirtual()) { - object = this.findOne(objectOrId, { - fields: { - [linker.linkStorageField]: 1, - }, - }); - } else { - object = { _id: objectOrId }; - } + const linker = linkData[name]; + let object = objectOrId; + if (typeof objectOrId == 'string') { + if (!linker.isVirtual()) { + object = await this.findOneAsync(objectOrId, { + fields: { + [linker.linkStorageField]: 1, + }, + }); + } else { + object = { _id: objectOrId }; + } - if (!object) { - throw new Meteor.Error( - `We could not find any object with _id: "${objectOrId}" within the collection: ${ - this._name - }` - ); - } - } + if (!object) { + throw new Meteor.Error( + `We could not find any object with _id: "${objectOrId}" within the collection: ${this._name}`, + ); + } + } - return linkData[name].createLink(object); - }, + return linkData[name].createLink(object); + }, }); diff --git a/lib/links/linkTypes/base.js b/lib/links/linkTypes/base.js index 4c57e1b4..137ca56d 100644 --- a/lib/links/linkTypes/base.js +++ b/lib/links/linkTypes/base.js @@ -1,216 +1,294 @@ import SmartArgs from './lib/smartArguments.js'; import createSearchFilters from '../lib/createSearchFilters'; - +import Linker from '../linker.js'; +import { _ } from 'meteor/underscore'; + +/** + * @class + * @property {Linker} linker + * @property {string} linkStorageField + * @property {Grapher.LinkObject} object + */ export default class Link { - get config() { return this.linker.linkConfig; } - - get isVirtual() { return this.linker.isVirtual() } - - constructor(linker, object, collection) { - this.linker = linker; - this.object = object; - this.linkedCollection = (collection) ? collection : linker.getLinkedCollection(); - - if (this.linker.isVirtual()) { - this.linkStorageField = this.config.relatedLinker.linkConfig.field; - } else { - this.linkStorageField = this.config.field; - } - } + get config() { + return this.linker.linkConfig; + } + + get isVirtual() { + return this.linker.isVirtual(); + } + + /** + * @param {Linker} linker + * @param {Grapher.LinkObject} object + * @param {Mongo.Collection | null} collection + */ + constructor(linker, object, collection) { + this.linker = linker; + this.object = object; /** - * Gets the stored link information value - * @returns {*} + * @private + * @type {Mongo.Collection} */ - value() { - if (this.isVirtual) { - throw new Meteor.Error('You can only take the value from the main link.'); - } - - return this.object[this.linkStorageField]; + this.linkedCollection = collection + ? collection + : linker.getLinkedCollection(); + + /** @type {string} */ + this.linkStorageField = this.linker.isVirtual() + ? this.config.relatedLinker?.linkConfig.field + : this.config.field; + } + + /** + * Gets the stored link information value + * @returns {*} + */ + value() { + if (this.isVirtual) { + throw new Meteor.Error('You can only take the value from the main link.'); } + return this.object[this.linkStorageField]; + } + + /** + * Finds linked data. + * + * @template T + * @template [R=T] + * @param {Grapher.DefaultFiltersWithMeta} filters + * @param {Mongo.Options | undefined} options + * @param {string | undefined} userId + * @returns {Mongo.Cursor} + */ + find(filters = {}, options = {}, userId = undefined) { + let linker = this.linker; /** - * Finds linked data. - * - * @param filters - * @param options - * @returns {*} - * @param userId + * @type {Mongo.Collection} */ - find(filters = {}, options = {}, userId = undefined) { - let linker = this.linker; - const linkedCollection = this.linkedCollection; - - let $metaFilters; - if (filters.$meta) { - $metaFilters = filters.$meta; - delete filters.$meta; - } - - const searchFilters = createSearchFilters( - this.object, - this.linker, - $metaFilters - ); + const linkedCollection = this.linkedCollection; - let appliedFilters = _.extend({}, filters, searchFilters); - - // console.log('search filters', searchFilters); - - // see https://github.com/cult-of-coders/grapher/issues/134 - // happens due to recursive importing of modules - // TODO: find another way to do this - if (linkedCollection.find) { - return linkedCollection.find(appliedFilters, options, userId) - } else { - return linkedCollection.default.find(appliedFilters, options, userId); - } + let $metaFilters; + if (filters.$meta) { + $metaFilters = filters.$meta; + delete filters.$meta; } + const searchFilters = createSearchFilters( + this.object, + this.linker, + $metaFilters, + ); + /** - * @param filters - * @param options - * @param others - * @returns {*|{content}|any} + * @type {Grapher.DefaultFiltersWithMeta} */ - fetch(filters, options, ...others) { - let result = this.find(filters, options, ...others).fetch(); + let appliedFilters = _.extend({}, filters, searchFilters); - if (this.linker.isOneResult()) { - return _.first(result); - } + // console.log('search filters', searchFilters); - return result; + // see https://github.com/cult-of-coders/grapher/issues/134 + // happens due to recursive importing of modules + // TODO: find another way to do this + if (linkedCollection.find) { + return linkedCollection.find(appliedFilters, options, userId); + } else { + return linkedCollection.default.find(appliedFilters, options, userId); } - - /** - * This is just like fetch() but forces to get an array even if it's single result - * - * @param {*} filters - * @param {*} options - * @param {...any} others - */ - fetchAsArray(filters, options, ...others) { - return this.find(filters, options, ...others).fetch() - } - + } + + /** + * @template T + * @template [R=T] + * @param {Mongo.Selector} [filters] + * @param {Mongo.Options} [options] + * @param {string} [userId] + * @returns {Promise} + */ + async fetch(filters, options, userId) { /** - * When we are dealing with multiple type relationships, $in would require an array. If the field value is null, it will fail - * We use clean to make it an empty array by default. + * @type {R[]} */ - clean() {} + let result = await this.find(filters, options, userId).fetchAsync(); - /** - * Extracts a single id - */ - identifyId(what, saveToDatabase) { - return SmartArgs.getId(what, { - saveToDatabase, - collection: this.linkedCollection - }); + if (this.linker.isOneResult()) { + return _.first(result); } - /** - * Extracts the ids of object(s) or strings and returns an array. - */ - identifyIds(what, saveToDatabase) { - return SmartArgs.getIds(what, { - saveToDatabase, - collection: this.linkedCollection - }); + return result; + } + + /** + * This is just like fetch() but forces to get an array even if it's single result + * + * @template T + * @template [R=T] + * @param {Mongo.Selector} [filters] + * @param {Mongo.Options} [options] + * @param {string | undefined} [userId] + * @returns {Promise} + */ + fetchAsArray(filters, options, userId) { + return this.find(filters, options, userId).fetchAsync(); + } + + /** + * When we are dealing with multiple type relationships, $in would require an array. If the field value is null, it will fail + * We use clean to make it an empty array by default. + */ + clean() {} + + /** + * @param {Grapher.IdSingleOption} what + * @param {boolean} [saveToDatabase] + * @returns {Promise} + * + * Extracts a single id + */ + identifyId(what, saveToDatabase) { + return SmartArgs.getId(what, { + saveToDatabase, + collection: this.linkedCollection, + }); + } + + /** + * + * @param {Grapher.IdOption} what + * @param {boolean} [saveToDatabase] + * + * Extracts the ids of object(s) or strings and returns an array. + */ + identifyIds(what, saveToDatabase) { + return SmartArgs.getIds(what, { + saveToDatabase, + collection: this.linkedCollection, + }); + } + + /** + * Checks when linking data, if the ids are valid with the linked collection. + * @throws Meteor.Error + * @param {string[] | string} ids + * @returns {Promise} + * + * @protected + */ + async _validateIds(ids) { + if (!Array.isArray(ids)) { + ids = [ids]; } - /** - * Checks when linking data, if the ids are valid with the linked collection. - * @throws Meteor.Error - * @param ids - * - * @protected - */ - _validateIds(ids) { - if (!Array.isArray(ids)) { - ids = [ids]; - } + // console.log('validate ids', ids); - // console.log('validate ids', ids); + const foreignIdentityField = this.linker.foreignIdentityField; - const foreignIdentityField = this.linker.foreignIdentityField; + const validIds = await this.linkedCollection + .find( + { + [foreignIdentityField]: { $in: ids }, + }, + { fields: { [foreignIdentityField]: 1 } }, + ) + .fetchAsync(); - const validIds = this.linkedCollection.find({ - [foreignIdentityField]: {$in: ids} - }, {fields: {[foreignIdentityField]: 1}}).fetch().map(doc => doc[foreignIdentityField]); + const mappedIds = validIds.map((doc) => doc[foreignIdentityField]); - if (validIds.length != ids.length) { - throw new Meteor.Error('not-found', `You tried to create links with non-existing id(s) inside "${this.linkedCollection._name}": ${_.difference(ids, validIds).join(', ')}`) - } + if (mappedIds.length != ids.length) { + throw new Meteor.Error( + 'not-found', + `You tried to create links with non-existing id(s) inside "${ + this.linkedCollection._name + }": ${_.difference(ids, validIds).join(', ')}`, + ); } - - _checkWhat(what) { - if (what === undefined || what === null) { - throw new Error(`The argument passed: ${what} is not accepted.`); - } + } + + /** + * @param {unknown} what + */ + _checkWhat(what) { + if (what === undefined || what === null) { + throw new Error(`The argument passed: ${what} is not accepted.`); + } + } + + /** + * This is for allowing commands such as set/unset/add/remove/metadata from the virtual link. + * + * @param {string} action + * @param {Grapher.IdOption} [what] + * @param {unknown} [metadata] + * @returns {Promise} + * + * @protected + */ + async _virtualAction(action, what, metadata) { + const linker = this.linker.linkConfig.relatedLinker; + if (!linker) { + throw new Error( + `The virtual link does not have a relatedLinker. Name=${this.linker.linkName}`, + ); } - /** - * This is for allowing commands such as set/unset/add/remove/metadata from the virtual link. - * - * @param action - * @param what - * @param metadata - * - * @protected - */ - _virtualAction(action, what, metadata) { - const linker = this.linker.linkConfig.relatedLinker; + // its an unset operation most likely. + if (what === undefined) { + const items = await this.fetch(); + const reversedLink = linker.createLink(items); + await reversedLink.unset(); - // its an unset operation most likely. - if (what === undefined) { - const reversedLink = linker.createLink(this.fetch()); - reversedLink.unset(); + return; + } - return; + const arrayOfWhats = Array.isArray(what) ? what : [what]; + + const docs = []; + for await (const element of arrayOfWhats) { + if (!_.isObject(element)) { + const doc = await linker.mainCollection.findOneAsync(element); + docs.push(doc); + } else { + if (!element._id) { + const elementId = await linker.mainCollection.insertAsync(element); + const doc = await linker.mainCollection.findOneAsync(elementId); + _.extend(element, doc); } + docs.push(element); + } + } - if (!Array.isArray(what)) { - what = [what]; + const processedDocs = []; + for await (const doc of docs) { + const reversedLink = linker.createLink(doc); + if (action == 'metadata') { + if (linker.isSingle()) { + const meta = await reversedLink.metadata(metadata); + processedDocs.push(meta); + return; + } else { + const meta = await reversedLink.metadata(this.object, metadata); + processedDocs.push(meta); + return; } + } else if (action == 'add' || action == 'set') { + if (linker.isSingle()) { + await reversedLink.set(this.object, metadata); + } else { + await reversedLink.add(this.object, metadata); + } + } else { + if (linker.isSingle()) { + await reversedLink.unset(); + } else { + await reversedLink.remove(this.object); + } + } - what = _.map(what, element => { - if (!_.isObject(element)) { - return linker.mainCollection.findOne(element); - } else { - if (!element._id) { - const elementId = linker.mainCollection.insert(element); - _.extend(element, linker.mainCollection.findOne(elementId)); - } - - return element; - } - }); - - return _.map(what, element => { - const reversedLink = linker.createLink(element); - - if (action == 'metadata') { - if (linker.isSingle()) { - return reversedLink.metadata(metadata); - } else { - return reversedLink.metadata(this.object, metadata); - } - } else if (action == 'add' || action == 'set') { - if (linker.isSingle()) { - reversedLink.set(this.object, metadata); - } else { - reversedLink.add(this.object, metadata); - } - } else { - if (linker.isSingle()) { - reversedLink.unset(); - } else { - reversedLink.remove(this.object); - } - } - }); + processedDocs.push(undefined); } + + return processedDocs; + } } diff --git a/lib/links/linkTypes/lib/smartArguments.js b/lib/links/linkTypes/lib/smartArguments.js index 5bf0200d..b8b34841 100644 --- a/lib/links/linkTypes/lib/smartArguments.js +++ b/lib/links/linkTypes/lib/smartArguments.js @@ -1,30 +1,51 @@ +import { _ } from 'meteor/underscore'; + /** * When you work with add/remove set/unset * You have the ability to pass strings, array of strings, objects, array of objects * If you are adding something and you want to save them in db, you can pass objects without ids. */ -export default new class { - getIds(what, options) { - if (Array.isArray(what)) { - return _.map(what, (subWhat) => { - return this.getId(subWhat, options); - }).filter(id => typeof id === 'string'); - } else { - return [this.getId(what, options)].filter(id => typeof id === 'string'); +export default new (class { + /** + * Extracts the ids of object(s) or strings and returns an array. + * @param {Grapher.IdOption} what + * @param {Grapher.SmartArgumentsOptions} [options] + * @returns {Promise} + */ + async getIds(what, options) { + if (Array.isArray(what)) { + /* @type {string[]} */ + const ids = []; + for await (const subWhat of what) { + const id = await this.getId(subWhat, options); + if (id) { + ids.push(id); } + } + return ids; + } else { + const id = await this.getId(what, options); + return id ? [id] : []; } + } - getId(what, options) { - if (typeof what === 'string') { - return what; - } + /** + * + * @param {Grapher.IdSingleOption} what + * @param {Grapher.SmartArgumentsOptions} [options] + * @returns {Promise} + */ + async getId(what, options = {}) { + if (typeof what === 'string') { + return what; + } - if (_.isObject(what)) { - if (!what._id && options.saveToDatabase) { - what._id = options.collection.insert(what); - } + if (_.isObject(what)) { + if (!what._id && options.saveToDatabase) { + what._id = await options.collection.insertAsync(what); + } - return what._id - } + return what._id; } -} + } +})(); diff --git a/lib/links/linkTypes/linkMany.js b/lib/links/linkTypes/linkMany.js index 681d0f61..48663183 100644 --- a/lib/links/linkTypes/linkMany.js +++ b/lib/links/linkTypes/linkMany.js @@ -1,115 +1,138 @@ import Link from './base.js'; import dot from 'dot-object'; -import SmartArgs from './lib/smartArguments.js'; +import { _ } from 'meteor/underscore'; export default class LinkMany extends Link { - clean() { - if (!this.object[this.linkStorageField]) { - this.object[this.linkStorageField] = []; - } + clean() { + if (!this.object[this.linkStorageField]) { + this.object[this.linkStorageField] = []; + } + } + + /** + * Ads the _ids to the object. + * @param {Grapher.IdOption} what + */ + async add(what) { + this._checkWhat(what); + + if (this.isVirtual) { + await this._virtualAction('add', what); + return this; } - /** - * Ads the _ids to the object. - * @param what - */ - add(what) { - this._checkWhat(what); + //if (this.isVirtual) throw new Meteor.Error('not-allowed', 'Add/remove operations must be done from the owning-link of the relationship'); - if (this.isVirtual) { - this._virtualAction('add', what); - return this; - } + this.clean(); - //if (this.isVirtual) throw new Meteor.Error('not-allowed', 'Add/remove operations must be done from the owning-link of the relationship'); + const _ids = await this.identifyIds(what, true); + await this._validateIds(_ids); - this.clean(); + const field = this.linkStorageField; - const _ids = this.identifyIds(what, true); - this._validateIds(_ids); + // update the field + this.object[field] = _.union(this.object[field], _ids); - const field = this.linkStorageField; + // update the db + let modifier = { + $addToSet: { + [field]: { $each: _ids }, + }, + }; - // update the field - this.object[field] = _.union(this.object[field], _ids); + await this.linker.mainCollection.updateAsync(this.object._id, modifier); - // update the db - let modifier = { - $addToSet: { - [field]: {$each: _ids} - } - }; + return this; + } - this.linker.mainCollection.update(this.object._id, modifier); + /** + * @param {unknown} what + */ + async remove(what) { + this._checkWhat(what); - return this; + if (this.isVirtual) { + await this._virtualAction('remove', what); + return this; } - /** - * @param what - */ - remove(what) { - this._checkWhat(what); - - if (this.isVirtual) { - this._virtualAction('remove', what); - return this; - } - - if (this.isVirtual) throw new Meteor.Error('not-allowed', 'Add/Remove operations should be done from the owner of the relationship'); - - this.clean(); - - const field = this.linkStorageField; - const [root, ...nested] = field.split('.'); - - const _ids = this.identifyIds(what); - - // update the field - this.object[root] = _.filter( - this.object[root], - _id => !_.contains(_ids, nested.length > 0 ? dot.pick(nested.join('.'), _id) : _id) - ); - - let modifier; - if (this.linker.foreignIdentityField === '_id') { - // update the db - modifier = { - $pullAll: { - [root]: nested.length > 0 ? { [nested.join('.')]: _ids } : _ids, - }, - }; - } - else { - modifier = { - $unset: {[root]: 1}, - }; - } - - this.linker.mainCollection.update(this.object._id, modifier); - - return this; + if (this.isVirtual) + throw new Meteor.Error( + 'not-allowed', + 'Add/Remove operations should be done from the owner of the relationship', + ); + + this.clean(); + + const field = this.linkStorageField; + const [root, ...nested] = field.split('.'); + + const _ids = await this.identifyIds(what); + + // update the field + this.object[root] = _.filter( + this.object[root], + (_id) => + !_.contains( + _ids, + nested.length > 0 ? dot.pick(nested.join('.'), _id) : _id, + ), + ); + + let modifier; + if (this.linker.foreignIdentityField === '_id') { + // update the db + modifier = { + $pullAll: { + [root]: nested.length > 0 ? { [nested.join('.')]: _ids } : _ids, + }, + }; + } else { + modifier = { + $unset: { [root]: 1 }, + }; } - set(what) { - this._checkWhat(what); - - if (this.isVirtual) { - this._virtualAction('set', what); - return this; - } + await this.linker.mainCollection.updateAsync(this.object._id, modifier); - throw new Meteor.Error('invalid-command', 'You are trying to *set* in a relationship that is many. Please use add/remove for *many* relationships'); - } + return this; + } - unset(what) { - this._checkWhat(what); + /** + * + * @param {Grapher.IdOption} what + * @returns + */ + async set(what) { + this._checkWhat(what); - if (this.isVirtual) { - this._virtualAction('unset', what); - return this; - } + if (this.isVirtual) { + await this._virtualAction('set', what); + return this; + } - throw new Meteor.Error('invalid-command', 'You are trying to *unset* in a relationship that is many. Please use add/remove for *many* relationships'); + throw new Meteor.Error( + 'invalid-command', + 'You are trying to *set* in a relationship that is many. Please use add/remove for *many* relationships', + ); + } + + /** + * + * @param {Grapher.IdOption} what + * @returns + */ + unset(what) { + this._checkWhat(what); + + if (this.isVirtual) { + this._virtualAction('unset', what); + return this; } + + throw new Meteor.Error( + 'invalid-command', + 'You are trying to *unset* in a relationship that is many. Please use add/remove for *many* relationships', + ); + } } diff --git a/lib/links/linkTypes/linkManyMeta.js b/lib/links/linkTypes/linkManyMeta.js index b7caa71c..c269ea9c 100644 --- a/lib/links/linkTypes/linkManyMeta.js +++ b/lib/links/linkTypes/linkManyMeta.js @@ -1,143 +1,177 @@ +import { _ } from 'meteor/underscore'; import Link from './base.js'; -import SmartArgs from './lib/smartArguments.js'; export default class LinkManyMeta extends Link { - clean() { - if (!this.object[this.linkStorageField]) { - this.object[this.linkStorageField] = []; - } + clean() { + if (!this.object[this.linkStorageField]) { + this.object[this.linkStorageField] = []; + } + } + + /** + * @param {Grapher.IdOption} what + * @param metadata + */ + async add(what, metadata = {}) { + this._checkWhat(what); + + if (this.isVirtual) { + await this._virtualAction('add', what, metadata); + return this; } - /** - * @param what - * @param metadata - */ - add(what, metadata = {}) { - this._checkWhat(what); - - if (this.isVirtual) { - this._virtualAction('add', what, metadata); - return this; - } - - const _ids = this.identifyIds(what, true); - this._validateIds(_ids); + const _ids = await this.identifyIds(what, true); + await this._validateIds(_ids); - let field = this.linkStorageField; + let field = this.linkStorageField; - this.object[field] = this.object[field] || []; - let metadatas = []; + this.object[field] = this.object[field] || []; + let metadatas = []; - _.each(_ids, _id => { - let localMetadata = _.clone(metadata); - localMetadata._id = _id; + _.each(_ids, (_id) => { + let localMetadata = _.clone(metadata); + localMetadata._id = _id; - this.object[field].push(localMetadata); - metadatas.push(localMetadata); - }); + this.object[field].push(localMetadata); + metadatas.push(localMetadata); + }); - let modifier = { - $addToSet: { - [field]: {$each: metadatas} - } - }; + let modifier = { + $addToSet: { + [field]: { $each: metadatas }, + }, + }; - this.linker.mainCollection.update(this.object._id, modifier); + await this.linker.mainCollection.updateAsync(this.object._id, modifier); - return this; - } + return this; + } - /** - * @param what - * @param extendMetadata - */ - metadata(what, extendMetadata) { - if (this.isVirtual) { - this._virtualAction('metadata', what, extendMetadata); - - return this; - } - - let field = this.linkStorageField; - - if (what === undefined) { - return this.object[field]; - } - - if (Array.isArray(what)) { - throw new Meteor.Error('not-allowed', 'Metadata updates should be made for one entity only, not multiple'); - } - - const _id = this.identifyId(what); - - let existingMetadata = _.find(this.object[field], metadata => metadata._id == _id); - if (extendMetadata === undefined) { - return existingMetadata; - } else { - _.extend(existingMetadata, extendMetadata); - let subfield = field + '._id'; - let subfieldUpdate = field + '.$'; - - this.linker.mainCollection.update({ - _id: this.object._id, - [subfield]: _id - }, { - $set: { - [subfieldUpdate]: existingMetadata - } - }); - } - - return this; + /** + * @param {Grapher.IdOption} what + * @param {unknown} extendMetadata + * @returns {Promise} + */ + async metadata(what, extendMetadata) { + if (this.isVirtual) { + await this._virtualAction('metadata', what, extendMetadata); + return this; } - remove(what) { - this._checkWhat(what); - - if (this.isVirtual) { - this._virtualAction('remove', what); - return this; - } - - const _ids = this.identifyIds(what); - let field = this.linkStorageField; + let field = this.linkStorageField; - this.object[field] = _.filter(this.object[field], link => !_.contains(_ids, link._id)); - - let modifier = { - $pull: { - [field]: { - _id: { - $in: _ids - } - } - } - }; + if (what === undefined) { + return this.object[field]; + } - this.linker.mainCollection.update(this.object._id, modifier); + if (Array.isArray(what)) { + throw new Meteor.Error( + 'not-allowed', + 'Metadata updates should be made for one entity only, not multiple', + ); + } - return this; + const _id = await this.identifyId(what); + + let existingMetadata = _.find( + this.object[field], + (metadata) => metadata._id == _id, + ); + if (extendMetadata === undefined) { + return existingMetadata; + } else { + _.extend(existingMetadata, extendMetadata); + let subfield = field + '._id'; + let subfieldUpdate = field + '.$'; + + await this.linker.mainCollection.updateAsync( + { + _id: this.object._id, + [subfield]: _id, + }, + { + $set: { + [subfieldUpdate]: existingMetadata, + }, + }, + ); } - set(what, metadata) { - this._checkWhat(what); + return this; + } - if (this.isVirtual) { - this._virtualAction('set', what, metadata); - return this; - } + /** + * + * @param {Grapher.IdOption} what + * @returns + */ + async remove(what) { + this._checkWhat(what); - throw new Meteor.Error('invalid-command', 'You are trying to *set* in a relationship that is single. Please use add/remove for *many* relationships'); + if (this.isVirtual) { + await this._virtualAction('remove', what); + return this; } - unset(what) { - this._checkWhat(what); - - if (this.isVirtual) { - this._virtualAction('unset', what); - return this; - } + const _ids = await this.identifyIds(what); + let field = this.linkStorageField; + + this.object[field] = _.filter( + this.object[field], + (link) => !_.contains(_ids, link._id), + ); + + let modifier = { + $pull: { + [field]: { + _id: { + $in: _ids, + }, + }, + }, + }; + + await this.linker.mainCollection.updateAsync(this.object._id, modifier); + + return this; + } + + /** + * + * @param {Grapher.IdOption} what + * @param {unknown} metadata + * @returns + */ + async set(what, metadata) { + this._checkWhat(what); + + if (this.isVirtual) { + await this._virtualAction('set', what, metadata); + return this; + } - throw new Meteor.Error('invalid-command', 'You are trying to *unset* in a relationship that is single. Please use add/remove for *many* relationships'); + throw new Meteor.Error( + 'invalid-command', + 'You are trying to *set* in a relationship that is single. Please use add/remove for *many* relationships', + ); + } + + /** + * + * @param {Grapher.IdOption} what + * @returns + */ + async unset(what) { + this._checkWhat(what); + + if (this.isVirtual) { + await this._virtualAction('unset', what); + return this; } + + throw new Meteor.Error( + 'invalid-command', + 'You are trying to *unset* in a relationship that is single. Please use add/remove for *many* relationships', + ); + } } diff --git a/lib/links/linkTypes/linkOne.js b/lib/links/linkTypes/linkOne.js index ef4fb468..a1759265 100644 --- a/lib/links/linkTypes/linkOne.js +++ b/lib/links/linkTypes/linkOne.js @@ -1,67 +1,82 @@ import Link from './base.js'; -import SmartArgs from './lib/smartArguments.js'; export default class LinkOne extends Link { - set(what) { - this._checkWhat(what); - - if (this.isVirtual) { - this._virtualAction('set', what); - return this; - } - - let field = this.linkStorageField; - const _id = this.identifyId(what, true); - this._validateIds([_id]); - - this.object[field] = _id; - - this.linker.mainCollection.update(this.object._id, { - $set: { - [field]: _id - } - }); - - return this; + /** + * + * @param {Grapher.IdSingleOption} what + * @returns + */ + async set(what) { + this._checkWhat(what); + + if (this.isVirtual) { + await this._virtualAction('set', what); + return this; } - unset() { - if (this.isVirtual) { - this._virtualAction('unset', what); - return this; - } - - let field = this.linkStorageField; - this.object[field] = null; + let field = this.linkStorageField; + const _id = await this.identifyId(what, true); + await this._validateIds([_id]); + + this.object[field] = _id; + + await this.linker.mainCollection.updateAsync(this.object._id, { + $set: { + [field]: _id, + }, + }); + + return this; + } + + /** + * + * @param {Grapher.IdSingleOption} what + * @returns + */ + async unset(what) { + if (this.isVirtual) { + await this._virtualAction('unset', what); + return this; + } - this.linker.mainCollection.update(this.object._id, { - $set: { - [field]: null - } - }); + let field = this.linkStorageField; + this.object[field] = null; - return this; - } + await this.linker.mainCollection.updateAsync(this.object._id, { + $set: { + [field]: null, + }, + }); - add(what) { - this._checkWhat(what); + return this; + } - if (this.isVirtual) { - this._virtualAction('add', what); - return this; - } + async add(what) { + this._checkWhat(what); - throw new Meteor.Error('invalid-command', 'You are trying to *add* in a relationship that is single. Please use set/unset for *single* relationships'); + if (this.isVirtual) { + await this._virtualAction('add', what); + return this; } - remove(what) { - this._checkWhat(what); + throw new Meteor.Error( + 'invalid-command', + 'You are trying to *add* in a relationship that is single. Please use set/unset for *single* relationships', + ); + } - if (this.isVirtual) { - this._virtualAction('remove', what); - return this; - } + async remove(what) { + this._checkWhat(what); - throw new Meteor.Error('invalid-command', 'You are trying to *remove* in a relationship that is single. Please use set/unset for *single* relationships'); + if (this.isVirtual) { + await this._virtualAction('remove', what); + return this; } -} \ No newline at end of file + + throw new Meteor.Error( + 'invalid-command', + 'You are trying to *remove* in a relationship that is single. Please use set/unset for *single* relationships', + ); + } +} diff --git a/lib/links/linkTypes/linkOneMeta.js b/lib/links/linkTypes/linkOneMeta.js index d8b5ee60..4f03a862 100644 --- a/lib/links/linkTypes/linkOneMeta.js +++ b/lib/links/linkTypes/linkOneMeta.js @@ -1,93 +1,114 @@ +import { _ } from 'meteor/underscore'; import Link from './base.js'; -import SmartArgs from './lib/smartArguments.js'; export default class LinkOneMeta extends Link { - set(what, metadata = {}) { - this._checkWhat(what); - - metadata = Object.assign({}, metadata); - - if (this.isVirtual) { - this._virtualAction('set', what, metadata); - return this; - } - - let field = this.linkStorageField; - metadata._id = this.identifyId(what, true); - this._validateIds([metadata._id]); - - this.object[field] = metadata; - - this.linker.mainCollection.update(this.object._id, { - $set: { - [field]: metadata - } - }); - - return this; + /** + * @param {Grapher.IdSingleOption} what + * @param {unknown} metadata + * @returns + */ + async set(what, metadata = {}) { + this._checkWhat(what); + + metadata = Object.assign({}, metadata); + + if (this.isVirtual) { + await this._virtualAction('set', what, metadata); + return this; } - metadata(extendMetadata) { - if (this.isVirtual) { - this._virtualAction('metadata', undefined, extendMetadata); - - return this; - } + let field = this.linkStorageField; + metadata._id = await this.identifyId(what, true); + await this._validateIds([metadata._id]); - let field = this.linkStorageField; + this.object[field] = metadata; - if (!extendMetadata) { - return this.object[field]; - } else { - _.extend(this.object[field], extendMetadata); + await this.linker.mainCollection.updateAsync(this.object._id, { + $set: { + [field]: metadata, + }, + }); - this.linker.mainCollection.update(this.object._id, { - $set: { - [field]: this.object[field] - } - }); - } + return this; + } - return this; + async metadata(extendMetadata) { + if (this.isVirtual) { + await this._virtualAction('metadata', undefined, extendMetadata); + return this; } - unset() { - if (this.isVirtual) { - this._virtualAction('unset'); - return this; - } + let field = this.linkStorageField; - let field = this.linkStorageField; - this.object[field] = {}; + if (!extendMetadata) { + return this.object[field]; + } else { + _.extend(this.object[field], extendMetadata); - this.linker.mainCollection.update(this.object._id, { - $set: { - [field]: {} - } - }); - - return this; + await this.linker.mainCollection.updateAsync(this.object._id, { + $set: { + [field]: this.object[field], + }, + }); } - add(what, metadata) { - this._checkWhat(what); - - if (this.isVirtual) { - this._virtualAction('add', what, metadata); - return this; - } + return this; + } - throw new Meteor.Error('invalid-command', 'You are trying to *add* in a relationship that is single. Please use set/unset for *single* relationships'); + async unset() { + if (this.isVirtual) { + await this._virtualAction('unset'); + return this; } - remove(what) { - this._checkWhat(what); - - if (this.isVirtual) { - this._virtualAction('remove', what); - return this; - } + let field = this.linkStorageField; + this.object[field] = {}; + + await this.linker.mainCollection.updateAsync(this.object._id, { + $set: { + [field]: {}, + }, + }); + + return this; + } + + /** + * + * @param {Grapher.IdOption} what + * @param {unknown} metadata + * @returns + */ + async add(what, metadata) { + this._checkWhat(what); + + if (this.isVirtual) { + await this._virtualAction('add', what, metadata); + return this; + } - throw new Meteor.Error('invalid-command', 'You are trying to *remove* in a relationship that is single. Please use set/unset for *single* relationships'); + throw new Meteor.Error( + 'invalid-command', + 'You are trying to *add* in a relationship that is single. Please use set/unset for *single* relationships', + ); + } + + /** + * + * @param {Grapher.IdOption} what + * @returns + */ + async remove(what) { + this._checkWhat(what); + + if (this.isVirtual) { + await this._virtualAction('remove', what); + return this; } -} \ No newline at end of file + + throw new Meteor.Error( + 'invalid-command', + 'You are trying to *remove* in a relationship that is single. Please use set/unset for *single* relationships', + ); + } +} diff --git a/lib/links/linker.js b/lib/links/linker.js index 5699c49e..a872a370 100644 --- a/lib/links/linker.js +++ b/lib/links/linker.js @@ -7,492 +7,491 @@ import smartArguments from './linkTypes/lib/smartArguments'; import dot from 'dot-object'; import { check } from 'meteor/check'; import { _ } from 'meteor/underscore'; -import { access } from 'fs'; export default class Linker { - /** - * @param mainCollection - * @param linkName - * @param linkConfig - */ - constructor(mainCollection, linkName, linkConfig) { - this.mainCollection = mainCollection; - this.linkConfig = Object.assign({}, LinkConfigDefaults, linkConfig); - this.linkName = linkName; - - // check linkName must not exist in schema - this._validateAndClean(); - - // initialize cascade removal hooks. - this._initAutoremove(); - this._initDenormalization(); - - if (this.isVirtual()) { - // if it's a virtual field make sure that when this is deleted, it will be removed from the references - if (!linkConfig.autoremove) { - this._handleReferenceRemovalForVirtualLinks(); - } - } else { - this._initIndex(); - } + /** + * TODO(v3): create static async function because of _initIndex()? + * + * @param {Mongo.Collection} mainCollection + * @param {string} linkName + * @param {Grapher.ProcessedLink} linkConfig + */ + constructor(mainCollection, linkName, linkConfig) { + this.mainCollection = mainCollection; + /** @type {Grapher.ProcessedLink} */ + this.linkConfig = Object.assign({}, LinkConfigDefaults, linkConfig); + this.linkName = linkName; + + // check linkName must not exist in schema + this._validateAndClean(); + + // initialize cascade removal hooks. + this._initAutoremove(); + this._initDenormalization(); + + if (this.isVirtual()) { + // if it's a virtual field make sure that when this is deleted, it will be removed from the references + if (!linkConfig.autoremove) { + this._handleReferenceRemovalForVirtualLinks(); + } + } else { + // TODO(v3): this is async now and we should await it + this._initIndex(); } - - /** - * Values which represent for the relation a single link - * @returns {string[]} - */ - get oneTypes() { - return ['one', '1']; + } + + /** + * Values which represent for the relation a single link + * @returns {string[]} + */ + get oneTypes() { + return ['one', '1']; + } + + /** + * Returns the strategies: one, many, one-meta, many-meta + * @returns {string} + */ + get strategy() { + let strategy = this.isMany() ? 'many' : 'one'; + if (this.linkConfig.metadata) { + strategy += '-meta'; } - /** - * Returns the strategies: one, many, one-meta, many-meta - * @returns {string} - */ - get strategy() { - let strategy = this.isMany() ? 'many' : 'one'; - if (this.linkConfig.metadata) { - strategy += '-meta'; - } + return strategy; + } - return strategy; + /** + * Returns the field name in the document where the actual relationships are stored. + * @returns {string} + */ + get linkStorageField() { + if (this.linkConfig.relatedLinker) { + return this.linkConfig.relatedLinker.linkStorageField; } - /** - * Returns the field name in the document where the actual relationships are stored. - * @returns string - */ - get linkStorageField() { - if (this.isVirtual()) { - return this.linkConfig.relatedLinker.linkStorageField; - } - - return this.linkConfig.field; + return this.linkConfig.field; + } + + /** + * Returns foreign field for querying linked collection + */ + get foreignIdentityField() { + if (this.isVirtual()) { + return ( + this.linkConfig.relatedLinker.linkConfig.foreignIdentityField || '_id' + ); } - - /** - * Returns foreign field for querying linked collection - */ - get foreignIdentityField() { - if (this.isVirtual()) { - return this.linkConfig.relatedLinker.linkConfig.foreignIdentityField || '_id'; - } - return this.linkConfig.foreignIdentityField || '_id'; + return this.linkConfig.foreignIdentityField || '_id'; + } + + /** + * The collection that is linked with the current collection + * @returns {Mongo.Collection} + */ + getLinkedCollection() { + return this.linkConfig.collection; + } + + /** + * If the relationship for this link is of "many" type. + */ + isMany() { + return !this.isSingle(); + } + + /** + * If the relationship for this link contains metadata + */ + isMeta() { + if (this.isVirtual()) { + return this.linkConfig.relatedLinker.isMeta(); } - /** - * The collection that is linked with the current collection - * @returns Mongo.Collection - */ - getLinkedCollection() { - return this.linkConfig.collection; + return !!this.linkConfig.metadata; + } + + /** + * @returns {boolean} + */ + isSingle() { + if (this.isVirtual()) { + return this.linkConfig.relatedLinker.isSingle(); } - /** - * If the relationship for this link is of "many" type. - */ - isMany() { - return !this.isSingle(); + return _.contains(this.oneTypes, this.linkConfig.type); + } + + /** + * @returns {boolean} + */ + isVirtual() { + return !!this.linkConfig.inversedBy; + } + + /** + * Should return a single result. + */ + isOneResult() { + return ( + (this.isVirtual() && this.linkConfig.relatedLinker?.linkConfig.unique) || + (!this.isVirtual() && this.isSingle()) + ); + } + + /** + * @param {unknown} object + * @param {Mongo.Collection | null} [collection] To impersonate the getLinkedCollection() of the "Linker" + * + * @returns {LinkOne|LinkMany|LinkManyMeta|LinkOneMeta} + */ + createLink(object, collection = null) { + let helperClass = this._getHelperClass(); + + return new helperClass(this, object, collection); + } + + /** + * @returns {*} + * @private + */ + _validateAndClean() { + if (!this.linkConfig.collection) { + throw new Meteor.Error( + 'invalid-config', + `For the link ${this.linkName} you did not provide a collection.`, + ); } - /** - * If the relationship for this link contains metadata - */ - isMeta() { - if (this.isVirtual()) { - return this.linkConfig.relatedLinker.isMeta(); - } + if (typeof this.linkConfig.collection === 'string') { + const collectionName = this.linkConfig.collection; + this.linkConfig.collection = Mongo.Collection.get(collectionName); - return !!this.linkConfig.metadata; + if (!this.linkConfig.collection) { + throw new Meteor.Error( + 'invalid-collection', + `Could not find a collection with the name: ${collectionName}`, + ); + } } - /** - * @returns {boolean} - */ - isSingle() { - if (this.isVirtual()) { - return this.linkConfig.relatedLinker.isSingle(); + if (this.isVirtual()) { + return this._prepareVirtual(); + } else { + if (!this.linkConfig.type) { + this.linkConfig.type = 'one'; + } + + if (!this.linkConfig.field) { + this.linkConfig.field = this._generateFieldName(); + } else { + if (this.linkConfig.field == this.linkName) { + throw new Meteor.Error( + 'invalid-config', + `For the link ${this.linkName} you must not use the same name for the field, otherwise it will cause conflicts when fetching data`, + ); } - - return _.contains(this.oneTypes, this.linkConfig.type); + } } - /** - * @returns {boolean} - */ - isVirtual() { - return !!this.linkConfig.inversedBy; + check(this.linkConfig, LinkConfigSchema); + } + + /** + * We need to apply same type of rules in this case. + * @private + */ + _prepareVirtual() { + const { collection, inversedBy } = this.linkConfig; + let linker = collection.getLinker(inversedBy); + + if (!linker) { + // it is possible that the collection doesn't have a linker created yet. + // so we will create it on startup after all links have been defined + Meteor.startup(() => { + linker = collection.getLinker(inversedBy); + if (!linker) { + throw new Meteor.Error( + `You tried setting up an inversed link in "${this.mainCollection._name}" pointing to collection: "${collection._name}" link: "${inversedBy}", but no such link was found. Maybe a typo ?`, + ); + } else { + this._setupVirtualConfig(linker); + } + }); + } else { + this._setupVirtualConfig(linker); } - - /** - * Should return a single result. - */ - isOneResult() { - return ( - (this.isVirtual() && - this.linkConfig.relatedLinker.linkConfig.unique) || - (!this.isVirtual() && this.isSingle()) - ); + } + + /** + * @param {Linker} linker + * @private + */ + _setupVirtualConfig(linker) { + const virtualLinkConfig = linker.linkConfig; + + if (!virtualLinkConfig) { + throw new Meteor.Error( + `There is no link-config for the related collection on ${inversedBy}. Make sure you added the direct links before specifying virtual ones.`, + ); } - /** - * @param object - * @param collection To impersonate the getLinkedCollection() of the "Linker" - * - * @returns {LinkOne|LinkMany|LinkManyMeta|LinkOneMeta|LinkResolve} - */ - createLink(object, collection = null) { - let helperClass = this._getHelperClass(); - - return new helperClass(this, object, collection); + _.extend(this.linkConfig, { + metadata: virtualLinkConfig.metadata, + relatedLinker: linker, + }); + } + + /** + * Depending on the strategy, we create the proper helper class + * @private + */ + _getHelperClass() { + switch (this.strategy) { + case 'many-meta': + return LinkManyMeta; + case 'many': + return LinkMany; + case 'one-meta': + return LinkOneMeta; + case 'one': + return LinkOne; } - /** - * @returns {*} - * @private - */ - _validateAndClean() { - if (!this.linkConfig.collection) { - throw new Meteor.Error( - 'invalid-config', - `For the link ${ - this.linkName - } you did not provide a collection.` - ); - } - - if (typeof this.linkConfig.collection === 'string') { - const collectionName = this.linkConfig.collection; - this.linkConfig.collection = Mongo.Collection.get(collectionName); - - if (!this.linkConfig.collection) { - throw new Meteor.Error( - 'invalid-collection', - `Could not find a collection with the name: ${collectionName}` - ); - } + throw new Meteor.Error( + 'invalid-strategy', + `${this.strategy} is not a valid strategy`, + ); + } + + /** + * If field name not present, we generate it. + * @private + * @returns {string | undefined} + */ + _generateFieldName() { + let cleanedCollectionName = this.linkConfig.collection._name.replace( + /\./g, + '_', + ); + let defaultFieldPrefix = this.linkName + '_' + cleanedCollectionName; + + switch (this.strategy) { + case 'many-meta': + return `${defaultFieldPrefix}_metas`; + case 'many': + return `${defaultFieldPrefix}_ids`; + case 'one-meta': + return `${defaultFieldPrefix}_meta`; + case 'one': + return `${defaultFieldPrefix}_id`; + } + } + + /** + * When a link that is declared virtual is removed, the reference will be removed from every other link. + * @private + */ + _handleReferenceRemovalForVirtualLinks() { + this.mainCollection.after.remove(async (userId, doc) => { + // this problem may occur when you do a .remove() before Meteor.startup() + if (!this.linkConfig.relatedLinker) { + console.warn( + `There was an error finding the link for removal for collection: "${this.mainCollection._name}" with link: "${this.linkName}". This may occur when you do a .remove() before Meteor.startup()`, + ); + return; + } + + const accessor = this.createLink(doc); + + const items = await accessor.fetchAsArray(); + for (const linkedObj of items) { + const { relatedLinker } = this.linkConfig; + // We do this check, to avoid self-referencing hell when defining virtual links + // Virtual links if not found "compile-time", we will try again to reprocess them on Meteor.startup + // if a removal happens before Meteor.startup this may fail + if (relatedLinker) { + let link = relatedLinker.createLink(linkedObj); + + if (relatedLinker.isSingle()) { + await link.unset(); + } else { + await link.remove(doc); + } } - + } + }); + } + + /** + * @returns {Promise} + */ + async _initIndex() { + if (Meteor.isServer) { + let field = this.linkConfig.field; + if (this.linkConfig.metadata) { + field = field + '._id'; + } + + if (this.linkConfig.index) { if (this.isVirtual()) { - return this._prepareVirtual(); - } else { - if (!this.linkConfig.type) { - this.linkConfig.type = 'one'; - } - - if (!this.linkConfig.field) { - this.linkConfig.field = this._generateFieldName(); - } else { - if (this.linkConfig.field == this.linkName) { - throw new Meteor.Error( - 'invalid-config', - `For the link ${ - this.linkName - } you must not use the same name for the field, otherwise it will cause conflicts when fetching data` - ); - } - } - } - - check(this.linkConfig, LinkConfigSchema); - } - - /** - * We need to apply same type of rules in this case. - * @private - */ - _prepareVirtual() { - const { collection, inversedBy } = this.linkConfig; - let linker = collection.getLinker(inversedBy); - - if (!linker) { - // it is possible that the collection doesn't have a linker created yet. - // so we will create it on startup after all links have been defined - Meteor.startup(() => { - linker = collection.getLinker(inversedBy); - if (!linker) { - throw new Meteor.Error( - `You tried setting up an inversed link in "${ - this.mainCollection._name - }" pointing to collection: "${ - collection._name - }" link: "${inversedBy}", but no such link was found. Maybe a typo ?` - ); - } else { - this._setupVirtualConfig(linker); - } - }); - } else { - this._setupVirtualConfig(linker); + throw new Meteor.Error('You cannot set index on an inversed link.'); } - } - /** - * @param linker - * @private - */ - _setupVirtualConfig(linker) { - const virtualLinkConfig = linker.linkConfig; - - if (!virtualLinkConfig) { + /** + * @type {import('mongodb').CreateIndexesOptions} + */ + const options = this.linkConfig.unique ? { unique: true } : {}; + await this.mainCollection.createIndexAsync({ [field]: 1 }, options); + // this.mainCollection.createIndex({ [field]: 1 }, options); + } else { + if (this.linkConfig.unique) { + if (this.isVirtual()) { throw new Meteor.Error( - `There is no link-config for the related collection on ${inversedBy}. Make sure you added the direct links before specifying virtual ones.` + 'You cannot set unique property on an inversed link.', ); - } + } + + /** + * @type {import('mongodb').CreateIndexesOptions} + */ + let options = { unique: true }; + + if (this.isSingle()) { + options = { ...options, sparse: true }; + } else { + options = { + ...options, + partialFilterExpression: { + [field]: { $type: 'string' }, + }, + }; + } - _.extend(this.linkConfig, { - metadata: virtualLinkConfig.metadata, - relatedLinker: linker, - }); - } + await this.mainCollection.createIndexAsync({ [field]: 1 }, options); - /** - * Depending on the strategy, we create the proper helper class - * @private - */ - _getHelperClass() { - switch (this.strategy) { - case 'many-meta': - return LinkManyMeta; - case 'many': - return LinkMany; - case 'one-meta': - return LinkOneMeta; - case 'one': - return LinkOne; + // this.mainCollection.createIndex( + // { + // [field]: 1, + // }, + // options, + // ); } + } + } + } - throw new Meteor.Error( - 'invalid-strategy', - `${this.strategy} is not a valid strategy` - ); + _initAutoremove() { + if (!this.linkConfig.autoremove) { + return; } - /** - * If field name not present, we generate it. - * @private - */ - _generateFieldName() { - let cleanedCollectionName = this.linkConfig.collection._name.replace( - /\./g, - '_' - ); - let defaultFieldPrefix = this.linkName + '_' + cleanedCollectionName; - - switch (this.strategy) { - case 'many-meta': - return `${defaultFieldPrefix}_metas`; - case 'many': - return `${defaultFieldPrefix}_ids`; - case 'one-meta': - return `${defaultFieldPrefix}_meta`; - case 'one': - return `${defaultFieldPrefix}_id`; + if (!this.isVirtual()) { + this.mainCollection.after.remove(async (userId, doc) => { + const ids = await smartArguments.getIds(doc[this.linkStorageField]); + if (ids.length > 0) { + await this.getLinkedCollection().removeAsync({ + [this.foreignIdentityField]: { + $in: ids, + }, + }); } - } + }); + } else { + this.mainCollection.after.remove(async (userId, doc) => { + const linker = await this.mainCollection.getLink(doc, this.linkName); + const docs = await linker.find({}, { fields: { _id: 1 } }).fetchAsync(); - /** - * When a link that is declared virtual is removed, the reference will be removed from every other link. - * @private - */ - _handleReferenceRemovalForVirtualLinks() { - this.mainCollection.after.remove((userId, doc) => { - // this problem may occur when you do a .remove() before Meteor.startup() - if (!this.linkConfig.relatedLinker) { - console.warn( - `There was an error finding the link for removal for collection: "${ - this.mainCollection._name - }" with link: "${ - this.linkName - }". This may occur when you do a .remove() before Meteor.startup()` - ); - return; - } - - const accessor = this.createLink(doc); - - _.each(accessor.fetchAsArray(), linkedObj => { - const { relatedLinker } = this.linkConfig; - // We do this check, to avoid self-referencing hell when defining virtual links - // Virtual links if not found "compile-time", we will try again to reprocess them on Meteor.startup - // if a removal happens before Meteor.startup this may fail - if (relatedLinker) { - let link = relatedLinker.createLink(linkedObj); - - if (relatedLinker.isSingle()) { - link.unset(); - } else { - link.remove(doc); - } - } - }); + const ids = docs.map((doc) => doc._id); + + await this.getLinkedCollection().removeAsync({ + _id: { $in: ids }, }); + }); } - - _initIndex() { - if (Meteor.isServer) { - let field = this.linkConfig.field; - if (this.linkConfig.metadata) { - field = field + '._id'; - } - - if (this.linkConfig.index) { - if (this.isVirtual()) { - throw new Meteor.Error( - 'You cannot set index on an inversed link.' - ); - } - - let options; - if (this.linkConfig.unique) { - options = { unique: true }; - } - - this.mainCollection._ensureIndex({ [field]: 1 }, options); - } else { - if (this.linkConfig.unique) { - if (this.isVirtual()) { - throw new Meteor.Error( - 'You cannot set unique property on an inversed link.' - ); - } - - let options = { unique: true }; - - if (this.isSingle()) { - options = {...options, sparse: true}; - } else { - options = {...options, - partialFilterExpression: { - [field]: { $type: 'string' } - } - } - } - - this.mainCollection._ensureIndex( - { - [field]: 1, - }, - options - ); - } - } - } + } + + /** + * Initializes denormalization using herteby:denormalize + * @private + */ + _initDenormalization() { + if (!this.linkConfig.denormalize || !Meteor.isServer) { + return; } - _initAutoremove() { - if (!this.linkConfig.autoremove) { - return; - } - - if (!this.isVirtual()) { - this.mainCollection.after.remove((userId, doc) => { - const ids = smartArguments.getIds(doc[this.linkStorageField]); - if (ids.length > 0) { - this.getLinkedCollection().remove({ - [this.foreignIdentityField]: { - $in: ids, - }, - }); - } - }); - } else { - this.mainCollection.after.remove((userId, doc) => { - const linker = this.mainCollection.getLink(doc, this.linkName); - const ids = linker - .find({}, { fields: { _id: 1 } }) - .fetch() - .map(item => item._id); - - this.getLinkedCollection().remove({ - _id: { $in: ids }, - }); - }); - } + const packageExists = !!Package['herteby:denormalize']; + if (!packageExists) { + throw new Meteor.Error( + 'missing-package', + `Please add the herteby:denormalize package to your Meteor application in order to make caching work`, + ); } - /** - * Initializes denormalization using herteby:denormalize - * @private - */ - _initDenormalization() { - if (!this.linkConfig.denormalize || !Meteor.isServer) { - return; - } - - const packageExists = !!Package['herteby:denormalize']; - if (!packageExists) { - throw new Meteor.Error( - 'missing-package', - `Please add the herteby:denormalize package to your Meteor application in order to make caching work` - ); - } + const { field, body, bypassSchema } = this.linkConfig.denormalize; + let cacheConfig; - const { field, body, bypassSchema } = this.linkConfig.denormalize; - let cacheConfig; - - let referenceFieldSuffix = ''; - if (this.isMeta()) { - referenceFieldSuffix = this.isSingle() ? '._id' : ':_id'; - } - - if (this.isVirtual()) { - let inversedLink = this.linkConfig.relatedLinker.linkConfig; - - let type = - inversedLink.type == 'many' ? 'many-inverse' : 'inversed'; - - cacheConfig = { - type: type, - collection: this.linkConfig.collection, - fields: body, - referenceField: inversedLink.field + referenceFieldSuffix, - cacheField: field, - bypassSchema: !!bypassSchema, - }; - } else { - cacheConfig = { - type: this.linkConfig.type, - collection: this.linkConfig.collection, - fields: body, - referenceField: this.linkConfig.field + referenceFieldSuffix, - cacheField: field, - bypassSchema: !!bypassSchema, - }; - } - - if (this.isVirtual()) { - Meteor.startup(() => { - this.mainCollection.cache(cacheConfig); - }); - } else { - this.mainCollection.cache(cacheConfig); - } + let referenceFieldSuffix = ''; + if (this.isMeta()) { + referenceFieldSuffix = this.isSingle() ? '._id' : ':_id'; } - /** - * Verifies if this linker is denormalized. It can be denormalized from the inverse side as well. - * - * @returns {boolean} - * @private - */ - isDenormalized() { - return !!this.linkConfig.denormalize; + if (this.isVirtual()) { + let inversedLink = this.linkConfig.relatedLinker.linkConfig; + + let type = inversedLink.type == 'many' ? 'many-inverse' : 'inversed'; + + cacheConfig = { + type: type, + collection: this.linkConfig.collection, + fields: body, + referenceField: inversedLink.field + referenceFieldSuffix, + cacheField: field, + bypassSchema: !!bypassSchema, + }; + } else { + cacheConfig = { + type: this.linkConfig.type, + collection: this.linkConfig.collection, + fields: body, + referenceField: this.linkConfig.field + referenceFieldSuffix, + cacheField: field, + bypassSchema: !!bypassSchema, + }; } - /** - * Verifies if the body of the linked element does not contain fields outside the cache body - * - * @param body - * @returns {boolean} - * @private - */ - isSubBodyDenormalized(body) { - const cacheBody = this.linkConfig.denormalize.body; - - const cacheBodyFields = _.keys(dot.dot(cacheBody)); - const bodyFields = _.keys(dot.dot(_.omit(body, '_id'))); - - return _.difference(bodyFields, cacheBodyFields).length === 0; + if (this.isVirtual()) { + Meteor.startup(() => { + this.mainCollection.cache(cacheConfig); + }); + } else { + this.mainCollection.cache(cacheConfig); } + } + + /** + * Verifies if this linker is denormalized. It can be denormalized from the inverse side as well. + * + * @returns {boolean} + * @private + */ + isDenormalized() { + return !!this.linkConfig.denormalize; + } + + /** + * Verifies if the body of the linked element does not contain fields outside the cache body + * + * @param body + * @returns {boolean} + * @private + */ + isSubBodyDenormalized(body) { + const cacheBody = this.linkConfig.denormalize.body; + + const cacheBodyFields = _.keys(dot.dot(cacheBody)); + const bodyFields = _.keys(dot.dot(_.omit(body, '_id'))); + + return _.difference(bodyFields, cacheBodyFields).length === 0; + } } diff --git a/lib/links/tests/client.test.js b/lib/links/tests/client.test.js index fe7e2bd6..0efe2147 100755 --- a/lib/links/tests/client.test.js +++ b/lib/links/tests/client.test.js @@ -1,31 +1,28 @@ -import { assert } from "chai"; - - -describe("Links Client Tests", function() { - - it("Test remove many", function() { - let PostCollection = new Mongo.Collection(null); - let CommentCollection = new Mongo.Collection(null); - - PostCollection.addLinks({ - comments: { - type: "many", - collection: CommentCollection, - field: "commentIds", - index: true, - } - }); - - let postId = PostCollection.insert({ text: "abc" }); - let commentId = CommentCollection.insert({ text: "abc" }); +import { assert } from 'chai'; + +describe('Links Client Tests', function () { + it('Test remove many', async function () { + let PostCollection = new Mongo.Collection(null); + let CommentCollection = new Mongo.Collection(null); + + PostCollection.addLinks({ + comments: { + type: 'many', + collection: CommentCollection, + field: 'commentIds', + index: true, + }, + }); - const link = PostCollection.getLink(postId, "comments"); - link.add(commentId); - assert.lengthOf(link.find().fetch(), 1); + let postId = PostCollection.insert({ text: 'abc' }); + let commentId = CommentCollection.insert({ text: 'abc' }); - link.remove(commentId); + const link = await PostCollection.getLink(postId, 'comments'); + await link.add(commentId); + assert.lengthOf(link.find().fetch(), 1); - assert.lengthOf(link.find().fetch(), 0); - }); + await link.remove(commentId); + assert.lengthOf(link.find().fetch(), 0); + }); }); diff --git a/lib/links/tests/main.js b/lib/links/tests/main.js index 7eddd793..5e0ae119 100644 --- a/lib/links/tests/main.js +++ b/lib/links/tests/main.js @@ -1,5 +1,6 @@ -import { assert, expect } from "chai"; +import { assert, expect } from 'chai'; import { Random } from 'meteor/random'; +import { _ } from 'meteor/underscore'; //import { // PostCollection, @@ -7,562 +8,612 @@ import { Random } from 'meteor/random'; // CommentCollection, // ResolverCollection //} from './collections.js'; -let PostCollection = new Mongo.Collection("test_post"); -let CategoryCollection = new Mongo.Collection("test_category"); -let CommentCollection = new Mongo.Collection("test_comment"); -let ResolverCollection = new Mongo.Collection("test_resolver"); +let PostCollection = new Mongo.Collection('test_post'); +let CategoryCollection = new Mongo.Collection('test_category'); +let CommentCollection = new Mongo.Collection('test_comment'); +let ResolverCollection = new Mongo.Collection('test_resolver'); let SCDCollection = new Mongo.Collection('test_scd'); let ReferenceCollection = new Mongo.Collection('test_scd_refs'); PostCollection.addLinks({ - comments: { - type: "*", - collection: CommentCollection, - field: "commentIds", - index: true - }, - commentsUnique: { - type: "*", - collection: CommentCollection, - field: "commentUniqueIds", - unique: true, - }, - autoRemoveComments: { - type: "*", - collection: CommentCollection, - field: "autoRemoveCommentIds", - autoremove: true - }, - autoRemovingSelfComments: { - type: "*", - collection: CommentCollection, - field: "autoRemovingSelfCommentsIds" - }, - metaComments: { - type: "*", - collection: CommentCollection, - metadata: true - }, - category: { - collection: CategoryCollection, - type: "1" - }, - metaCategory: { - metadata: true, - collection: CategoryCollection, - type: "1" - }, - inversedComment: { - collection: CommentCollection, - inversedBy: "inversedPost" - } + comments: { + type: '*', + collection: CommentCollection, + field: 'commentIds', + index: true, + }, + commentsUnique: { + type: '*', + collection: CommentCollection, + field: 'commentUniqueIds', + unique: true, + }, + autoRemoveComments: { + type: '*', + collection: CommentCollection, + field: 'autoRemoveCommentIds', + autoremove: true, + }, + autoRemovingSelfComments: { + type: '*', + collection: CommentCollection, + field: 'autoRemovingSelfCommentsIds', + }, + metaComments: { + type: '*', + collection: CommentCollection, + metadata: true, + }, + category: { + collection: CategoryCollection, + type: '1', + }, + metaCategory: { + metadata: true, + collection: CategoryCollection, + type: '1', + }, + inversedComment: { + collection: CommentCollection, + inversedBy: 'inversedPost', + }, }); CommentCollection.addLinks({ - post: { - collection: PostCollection, - inversedBy: "comments" - }, - postUnique: { - collection: PostCollection, - inversedBy: "commentsUnique" - }, - inversedPost: { - collection: PostCollection, - field: "postId" - }, - autoRemovePosts: { - collection: PostCollection, - inversedBy: "autoRemovingSelfComments", - autoremove: true - }, - metaPost: { - collection: PostCollection, - inversedBy: "metaComments" - } + post: { + collection: PostCollection, + inversedBy: 'comments', + }, + postUnique: { + collection: PostCollection, + inversedBy: 'commentsUnique', + }, + inversedPost: { + collection: PostCollection, + field: 'postId', + }, + autoRemovePosts: { + collection: PostCollection, + inversedBy: 'autoRemovingSelfComments', + autoremove: true, + }, + metaPost: { + collection: PostCollection, + inversedBy: 'metaComments', + }, }); CategoryCollection.addLinks({ - posts: { - collection: PostCollection, - inversedBy: "category" - }, - metaPosts: { - collection: PostCollection, - inversedBy: "metaCategory" - } + posts: { + collection: PostCollection, + inversedBy: 'category', + }, + metaPosts: { + collection: PostCollection, + inversedBy: 'metaCategory', + }, }); ReferenceCollection.addLinks({ - scds: { - type: 'many', - collection: SCDCollection, - field: 'scdId', - foreignIdentityField: 'originalId', - }, - scd: { - collection: SCDCollection, - inversedBy: 'ref', - }, -}) + scds: { + type: 'many', + collection: SCDCollection, + field: 'scdId', + foreignIdentityField: 'originalId', + }, + scd: { + collection: SCDCollection, + inversedBy: 'ref', + }, +}); SCDCollection.addLinks({ - refs: { - collection: ReferenceCollection, - inversedBy: 'scds', - autoremove: true, - }, - ref: { - type: 'one', - collection: ReferenceCollection, - field: 'someId', - foreignIdentityField: 'some2Id', - autoremove: true, - }, + refs: { + collection: ReferenceCollection, + inversedBy: 'scds', + autoremove: true, + }, + ref: { + type: 'one', + collection: ReferenceCollection, + field: 'someId', + foreignIdentityField: 'some2Id', + autoremove: true, + }, }); -describe("Collection Links", function() { - PostCollection.remove({}); - CategoryCollection.remove({}); - CommentCollection.remove({}); +describe('Collection Links', function () { + before(async () => { + await PostCollection.removeAsync({}); + await CategoryCollection.removeAsync({}); + await CommentCollection.removeAsync({}); + }); - it("Test Many", function() { - let postId = PostCollection.insert({ text: "abc" }); - let commentId = CommentCollection.insert({ text: "abc" }); + it('Test Many', async function () { + let postId = await PostCollection.insertAsync({ text: 'abc' }); + let commentId = await CommentCollection.insertAsync({ text: 'abc' }); - let post = PostCollection.findOne(postId); - const link = PostCollection.getLink(post, "comments"); - link.add(commentId); - assert.lengthOf(link.find().fetch(), 1); + let post = await PostCollection.findOneAsync(postId); + const link = await PostCollection.getLink(post, 'comments'); + await link.add(commentId); + assert.lengthOf(await link.find().fetchAsync(), 1); - link.remove(commentId); - assert.lengthOf(link.find().fetch(), 0); - }); - - it("Tests One", function() { - let postId = PostCollection.insert({ text: "abc" }); - let categoryId = CategoryCollection.insert({ text: "abc" }); + await link.remove(commentId); + assert.lengthOf(await link.find().fetchAsync(), 0); + }); - let post = PostCollection.findOne(postId); + it('Tests One', async function () { + let postId = await PostCollection.insertAsync({ text: 'abc' }); + let categoryId = await CategoryCollection.insertAsync({ text: 'abc' }); - const link = PostCollection.getLink(post, "category"); - link.set(categoryId); - assert.lengthOf(link.find().fetch(), 1); + let post = await PostCollection.findOneAsync(postId); - assert.equal(categoryId, link.fetch()._id); + const link = await PostCollection.getLink(post, 'category'); + await link.set(categoryId); + assert.lengthOf(await link.find().fetchAsync(), 1); - link.unset(); - assert.lengthOf(link.find().fetch(), 0); - }); + assert.equal(categoryId, (await link.fetch())._id); - it("Tests One Meta", function() { - let postId = PostCollection.insert({ text: "abc" }); - let categoryId = CategoryCollection.insert({ text: "abc" }); + await link.unset(); + assert.lengthOf(await link.find().fetchAsync(), 0); + }); - let post = PostCollection.findOne(postId); + it('Tests One Meta', async function () { + let postId = await PostCollection.insertAsync({ text: 'abc' }); + let categoryId = await CategoryCollection.insertAsync({ text: 'abc' }); - let link = PostCollection.getLink(post, "metaCategory"); - link.set(categoryId, { date: new Date() }); + let post = await PostCollection.findOneAsync(postId); - assert.lengthOf(link.find().fetch(), 1); - let metadata = link.metadata(); + let link = await PostCollection.getLink(post, 'metaCategory'); + await link.set(categoryId, { date: new Date() }); - assert.isObject(metadata); - assert.instanceOf(metadata.date, Date); + assert.lengthOf(await link.find().fetchAsync(), 1); + let metadata = await link.metadata(); - link.metadata({ - updated: new Date() - }); + assert.isObject(metadata); + assert.instanceOf(metadata.date, Date); - post = PostCollection.findOne(postId); - link = PostCollection.getLink(post, "metaCategory"); - assert.instanceOf(link.metadata().updated, Date); - - link.unset(); - assert.lengthOf(link.find().fetch(), 0); + await link.metadata({ + updated: new Date(), }); - it("Tests Many Meta", function() { - let postId = PostCollection.insert({ text: "abc" }); - let commentId = CommentCollection.insert({ text: "abc" }); + post = await PostCollection.findOneAsync(postId); + link = await PostCollection.getLink(post, 'metaCategory'); + assert.instanceOf((await link.metadata()).updated, Date); - let post = PostCollection.findOne(postId); - let metaCommentsLink = PostCollection.getLink(post, "metaComments"); + await link.unset(); + assert.lengthOf(await link.find().fetchAsync(), 0); + }); - metaCommentsLink.add(commentId, { date: new Date() }); - assert.lengthOf(metaCommentsLink.find().fetch(), 1); + it('Tests Many Meta', async function () { + let postId = await PostCollection.insertAsync({ text: 'abc' }); + let commentId = await CommentCollection.insertAsync({ text: 'abc' }); - // verifying reverse search - let metaComment = CommentCollection.findOne(commentId); - let metaPostLink = CommentCollection.getLink(metaComment, "metaPost"); - assert.lengthOf(metaPostLink.find().fetch(), 1); + let post = await PostCollection.findOneAsync(postId); + let metaCommentsLink = await PostCollection.getLink(post, 'metaComments'); - let metadata = metaCommentsLink.metadata(commentId); + await metaCommentsLink.add(commentId, { date: new Date() }); + assert.lengthOf(await metaCommentsLink.find().fetchAsync(), 1); - assert.isObject(metadata); - assert.instanceOf(metadata.date, Date); + // verifying reverse search + let metaComment = await CommentCollection.findOneAsync(commentId); + let metaPostLink = await CommentCollection.getLink(metaComment, 'metaPost'); + assert.lengthOf(await metaPostLink.find().fetchAsync(), 1); - metaCommentsLink.metadata(commentId, { updated: new Date() }); + let metadata = await metaCommentsLink.metadata(commentId); - post = PostCollection.findOne(postId); - metaCommentsLink = PostCollection.getLink(post, "metaComments"); + assert.isObject(metadata); + assert.instanceOf(metadata.date, Date); - metadata = metaCommentsLink.metadata(commentId); - assert.instanceOf(metadata.updated, Date); + await metaCommentsLink.metadata(commentId, { updated: new Date() }); - metaCommentsLink.remove(commentId); - assert.lengthOf(metaCommentsLink.find().fetch(), 0); - }); + post = await PostCollection.findOneAsync(postId); + metaCommentsLink = await PostCollection.getLink(post, 'metaComments'); - it("Tests $meta filters for One & One-Virtual", function() { - let postId = PostCollection.insert({ text: "abc" }); - let categoryId = CategoryCollection.insert({ text: "abc" }); - let post = PostCollection.findOne(postId); - let postMetaCategoryLink = PostCollection.getLink(post, "metaCategory"); - postMetaCategoryLink.set(categoryId, { valid: true }); + metadata = await metaCommentsLink.metadata(commentId); + assert.instanceOf(metadata.updated, Date); - let result = postMetaCategoryLink.fetch({ $meta: { valid: true } }); - assert.isObject(result); + await metaCommentsLink.remove(commentId); + assert.lengthOf(await metaCommentsLink.find().fetchAsync(), 0); + }); - result = postMetaCategoryLink.fetch({ $meta: { valid: false } }); + it('Tests $meta filters for One & One-Virtual', async function () { + let postId = await PostCollection.insertAsync({ text: 'abc' }); + let categoryId = await CategoryCollection.insertAsync({ text: 'abc' }); + let post = await PostCollection.findOneAsync(postId); + let postMetaCategoryLink = await PostCollection.getLink( + post, + 'metaCategory', + ); + await postMetaCategoryLink.set(categoryId, { valid: true }); - assert.isUndefined(result); - const metaCategoryPostLink = CategoryCollection.getLink( - categoryId, - "metaPosts" - ); + let result = await postMetaCategoryLink.fetch({ $meta: { valid: true } }); + assert.isObject(result); - result = metaCategoryPostLink.fetch({ $meta: { valid: true } }); - assert.lengthOf(result, 1); + result = await postMetaCategoryLink.fetch({ $meta: { valid: false } }); - result = metaCategoryPostLink.fetch({ $meta: { valid: false } }); - assert.lengthOf(result, 0); - }); - - it("Tests $meta filters for Many & Many-Virtual", function() { - let postId = PostCollection.insert({ text: "abc" }); - let commentId1 = CommentCollection.insert({ text: "abc" }); - let commentId2 = CommentCollection.insert({ text: "abc" }); - - let postMetaCommentsLink = PostCollection.getLink( - postId, - "metaComments" - ); - - postMetaCommentsLink.add(commentId1, { approved: true }); - postMetaCommentsLink.add(commentId2, { approved: false }); - - let result = postMetaCommentsLink.fetch({ $meta: { approved: true } }); + assert.isUndefined(result); + const metaCategoryPostLink = await CategoryCollection.getLink( + categoryId, + 'metaPosts', + ); - assert.lengthOf(result, 1); + result = await metaCategoryPostLink.fetch({ $meta: { valid: true } }); + assert.lengthOf(result, 1); - result = postMetaCommentsLink.fetch({ $meta: { approved: false } }); + result = await metaCategoryPostLink.fetch({ $meta: { valid: false } }); + assert.lengthOf(result, 0); + }); - assert.lengthOf(result, 1); - - const comment1MetaPostsLink = CommentCollection.getLink( - commentId1, - "metaPost" - ); - result = comment1MetaPostsLink.fetch({ $meta: { approved: true } }); - assert.lengthOf(result, 1); - result = comment1MetaPostsLink.fetch({ $meta: { approved: false } }); - assert.lengthOf(result, 0); - - const comment2MetaPostsLink = CommentCollection.getLink( - commentId2, - "metaPost" - ); - result = comment2MetaPostsLink.fetch({ $meta: { approved: true } }); - assert.lengthOf(result, 0); - result = comment2MetaPostsLink.fetch({ $meta: { approved: false } }); - assert.lengthOf(result, 1); - }); - - it("Tests inversedBy findings", function() { - let postId = PostCollection.insert({ text: "abc" }); - let commentId = CommentCollection.insert({ text: "abc" }); - - let post = PostCollection.findOne(postId); - let comment = CommentCollection.findOne(commentId); - let commentsLink = PostCollection.getLink(post, "comments"); - let commentsUniqueLink = PostCollection.getLink(post, "commentsUnique"); - let metaCommentsLink = PostCollection.getLink(post, "metaComments"); - let postLink = CommentCollection.getLink(comment, "post"); - let postUniqueLink = CommentCollection.getLink(comment, "postUnique"); - let postMetaLink = CommentCollection.getLink(comment, "metaPost"); - - commentsLink.add(comment); - commentsUniqueLink.add(comment); - metaCommentsLink.add(comment); - assert.lengthOf(postLink.find().fetch(), 1); - assert.isObject(postUniqueLink.fetch()); - assert.lengthOf(postMetaLink.find().fetch(), 1); - - post = PostCollection.findOne(postId); - - CommentCollection.remove(comment._id); - post = PostCollection.findOne(postId); - assert.notInclude(post.commentIds, comment._id); - }); + it('Tests $meta filters for Many & Many-Virtual', async function () { + let postId = await PostCollection.insertAsync({ text: 'abc' }); + let commentId1 = await CommentCollection.insertAsync({ text: 'abc' }); + let commentId2 = await CommentCollection.insertAsync({ text: 'abc' }); - it("Should auto-save object", function() { - let comment = { text: "abc" }; + let postMetaCommentsLink = await PostCollection.getLink( + postId, + 'metaComments', + ); - let postId = PostCollection.insert({ text: "hello" }); - const postLink = PostCollection.getLink(postId, "comments").add( - comment - ); + await postMetaCommentsLink.add(commentId1, { approved: true }); + await postMetaCommentsLink.add(commentId2, { approved: false }); - assert.isDefined(comment._id); - assert.lengthOf(postLink.fetch(), 1); + let result = await postMetaCommentsLink.fetch({ + $meta: { approved: true }, }); - it("Should have indexes set up", function() { - const raw = PostCollection.rawCollection(); - const indexes = Meteor.wrapAsync(raw.indexes, raw)(); - - const found = _.find(indexes, index => { - return index.key.commentIds == 1; - }); - - assert.isObject(found); + assert.lengthOf(result, 1); + + result = await postMetaCommentsLink.fetch({ $meta: { approved: false } }); + + assert.lengthOf(result, 1); + + const comment1MetaPostsLink = await CommentCollection.getLink( + commentId1, + 'metaPost', + ); + result = await comment1MetaPostsLink.fetch({ $meta: { approved: true } }); + assert.lengthOf(result, 1); + result = await comment1MetaPostsLink.fetch({ $meta: { approved: false } }); + assert.lengthOf(result, 0); + + const comment2MetaPostsLink = await CommentCollection.getLink( + commentId2, + 'metaPost', + ); + result = await comment2MetaPostsLink.fetch({ $meta: { approved: true } }); + assert.lengthOf(result, 0); + result = await comment2MetaPostsLink.fetch({ $meta: { approved: false } }); + assert.lengthOf(result, 1); + }); + + it('Tests inversedBy findings', async function () { + let postId = await PostCollection.insertAsync({ text: 'abc' }); + let commentId = await CommentCollection.insertAsync({ text: 'abc' }); + + let post = await PostCollection.findOneAsync(postId); + let comment = await CommentCollection.findOneAsync(commentId); + let commentsLink = await PostCollection.getLink(post, 'comments'); + let commentsUniqueLink = await PostCollection.getLink( + post, + 'commentsUnique', + ); + let metaCommentsLink = await PostCollection.getLink(post, 'metaComments'); + let postLink = await CommentCollection.getLink(comment, 'post'); + let postUniqueLink = await CommentCollection.getLink(comment, 'postUnique'); + let postMetaLink = await CommentCollection.getLink(comment, 'metaPost'); + + await commentsLink.add(comment); + await commentsUniqueLink.add(comment); + await metaCommentsLink.add(comment); + + assert.lengthOf(await postLink.find().fetchAsync(), 1); + assert.isObject(await postUniqueLink.fetch()); + assert.lengthOf(await postMetaLink.find().fetchAsync(), 1); + + post = await PostCollection.findOneAsync(postId); + + const removeRes = await CommentCollection.removeAsync(comment._id); + + post = await PostCollection.findOneAsync(postId); + assert.notInclude(post.commentIds, comment._id); + }); + + it('Should auto-save object', async function () { + let comment = { text: 'abc' }; + + let postId = await PostCollection.insertAsync({ text: 'hello' }); + const postLink = await PostCollection.getLink(postId, 'comments'); + await postLink.add(comment); + + assert.isDefined(comment._id); + assert.lengthOf(await postLink.fetch(), 1); + }); + + it('Should have indexes set up', async function () { + /** + * @type {import('mongodb').Collection} + */ + const raw = PostCollection.rawCollection(); + const indexes = await raw.indexes(); + + const found = _.find(indexes, (index) => { + return index.key.commentIds == 1; }); - it("Should auto-remove some objects", function() { - let comment = { text: "abc" }; - - let postId = PostCollection.insert({ text: "hello" }); - let postLink = PostCollection.getLink(postId, "comments").add(comment); + assert.isObject(found); + }); - assert.isNotNull(comment._id); - PostCollection.remove(postId); - assert.isNotNull(CommentCollection.findOne(comment._id)); - - comment = { text: "abc" }; - postId = PostCollection.insert({ text: "hello" }); - postLink = PostCollection.getLink(postId, "autoRemoveComments").add( - comment - ); - - assert.isDefined(comment._id); - PostCollection.remove(postId); - assert.isUndefined(CommentCollection.findOne(comment._id)); - }); + it('Should auto-remove some objects', async function () { + let comment = { text: 'abc' }; - it("Should allow actions from inversed links", function() { - let comment = { text: "abc" }; + let postId = await PostCollection.insertAsync({ text: 'hello' }); + let postLink = await PostCollection.getLink(postId, 'comments'); + await postLink.add(comment); - let postId = PostCollection.insert({ text: "hello" }); - const commentId = CommentCollection.insert(comment); + assert.isNotNull(comment._id); + await PostCollection.removeAsync(postId); + assert.isNotNull(await CommentCollection.findOneAsync(comment._id)); - CommentCollection.getLink(commentId, "post").set(postId); + comment = { text: 'abc' }; + postId = await PostCollection.insertAsync({ text: 'hello' }); + postLink = await PostCollection.getLink(postId, 'autoRemoveComments'); + await postLink.add(comment); - assert.lengthOf(PostCollection.getLink(postId, "comments").fetch(), 1); + assert.isDefined(comment._id); + await PostCollection.removeAsync(postId); + assert.isUndefined(await CommentCollection.findOneAsync(comment._id)); + }); - CommentCollection.getLink(commentId, "post").add({ text: "hi there" }); + it('Should allow actions from inversed links', async function () { + let abcComment = { text: 'abc' }; - let insertedPostViaVirtual = PostCollection.findOne({ - text: "hi there" - }); - assert.isObject(insertedPostViaVirtual); + let helloPostId = await PostCollection.insertAsync({ text: 'hello' }); + const abcCommentId = await CommentCollection.insertAsync(abcComment); - assert.lengthOf( - PostCollection.getLink(insertedPostViaVirtual, "comments").fetch(), - 1 - ); + let abcCommentToPostLink = await CommentCollection.getLink( + abcCommentId, + 'post', + ); + await abcCommentToPostLink.set(helloPostId); - const category = CategoryCollection.findOne(); - let postsCategoryLink = CategoryCollection.getLink(category, "posts"); - postsCategoryLink.add(insertedPostViaVirtual); + const helloPostToCommentsLink = await PostCollection.getLink( + helloPostId, + 'comments', + ); - assert.equal( - category._id, - PostCollection.getLink(insertedPostViaVirtual, "category").fetch() - ._id - ); + const helloPostComments = await helloPostToCommentsLink.fetch(); - // TESTING META - let categoryMetaPostLink = CategoryCollection.getLink( - category, - "metaPosts" - ); - categoryMetaPostLink.add(insertedPostViaVirtual, { - testValue: "boom!" - }); + assert.lengthOf(helloPostComments, 1); + assert.equal(helloPostComments[0]._id, abcCommentId); - let postMetaCategoryLink = PostCollection.getLink( - insertedPostViaVirtual, - "metaCategory" - ); - assert.equal("boom!", postMetaCategoryLink.metadata().testValue); + // Add "hi there" post + abcCommentToPostLink = await CommentCollection.getLink( + abcCommentId, + 'post', + ); + await abcCommentToPostLink.add({ + text: 'hi there', }); - it("Should fail when you try to add a non-existing link", function(done) { - let postId = PostCollection.insert({ text: "hello" }); - - try { - PostCollection.getLink(postId, "comments").add("XXXXXXX"); - } catch (e) { - assert.equal(e.error, "not-found"); - done(); - } + let insertedPostViaVirtual = await PostCollection.findOneAsync({ + text: 'hi there', + }); + assert.isObject(insertedPostViaVirtual); + + assert.lengthOf( + await ( + await PostCollection.getLink(insertedPostViaVirtual, 'comments') + ).fetch(), + 1, + ); + + let categoryId = await CategoryCollection.insertAsync({ text: 'abc' }); + const category = await CategoryCollection.findOneAsync(categoryId); + let postsCategoryLink = await CategoryCollection.getLink(category, 'posts'); + await postsCategoryLink.add(insertedPostViaVirtual); + + assert.equal( + category._id, + ( + await ( + await PostCollection.getLink(insertedPostViaVirtual, 'category') + ).fetch() + )._id, + ); + + // TESTING META + let categoryMetaPostLink = await CategoryCollection.getLink( + category, + 'metaPosts', + ); + await categoryMetaPostLink.add(insertedPostViaVirtual, { + testValue: 'boom!', }); - it("Should work with autoremoval from inversed and direct link", function() { - // autoremoval from direct side - let postId = PostCollection.insert({ text: "autoremove" }); - const postAutoRemoveCommentsLink = PostCollection.getLink( - postId, - "autoRemoveComments" - ); - - postAutoRemoveCommentsLink.add({ text: "hello" }); - - assert.lengthOf(postAutoRemoveCommentsLink.find().fetch(), 1); - let commentId = postAutoRemoveCommentsLink.find().fetch()[0]._id; - - assert.isObject(CommentCollection.findOne(commentId)); - PostCollection.remove(postId); - assert.isUndefined(CommentCollection.findOne(commentId)); - - // now from inversed side - commentId = CommentCollection.insert({ text: "autoremove" }); + let postMetaCategoryLink = await PostCollection.getLink( + insertedPostViaVirtual, + 'metaCategory', + ); + assert.equal('boom!', (await postMetaCategoryLink.metadata()).testValue); + }); - const commentAutoRemovePostsLink = CommentCollection.getLink( - commentId, - "autoRemovePosts" - ); - commentAutoRemovePostsLink.add({ text: "Hello" }); + it('Should fail when you try to add a non-existing link', async function () { + let postId = await PostCollection.insertAsync({ text: 'hello' }); - assert.lengthOf(commentAutoRemovePostsLink.find().fetch(), 1); - postId = commentAutoRemovePostsLink.find().fetch()[0]._id; + try { + const link = await PostCollection.getLink(postId, 'comments'); + await link.add('XXXXXXX'); - assert.isObject(PostCollection.findOne(postId)); - CommentCollection.remove(commentId); - assert.isUndefined(PostCollection.findOne(postId)); + assert.fail('Should have thrown an error'); + } catch (e) { + assert.equal(e.error, 'not-found'); + } + }); + + it('Should work with autoremoval from inversed and direct link', async function () { + // autoremoval from direct side + let postId = await PostCollection.insertAsync({ text: 'autoremove' }); + const postAutoRemoveCommentsLink = await PostCollection.getLink( + postId, + 'autoRemoveComments', + ); + + await postAutoRemoveCommentsLink.add({ text: 'hello' }); + + const comments = await postAutoRemoveCommentsLink.find().fetchAsync(); + assert.lengthOf(comments, 1); + let commentId = comments[0]._id; + + assert.isObject(await CommentCollection.findOneAsync(commentId)); + await PostCollection.removeAsync(postId); + assert.isUndefined(await CommentCollection.findOneAsync(commentId)); + + // now from inversed side + commentId = await CommentCollection.insertAsync({ text: 'autoremove' }); + + const commentAutoRemovePostsLink = await CommentCollection.getLink( + commentId, + 'autoRemovePosts', + ); + await commentAutoRemovePostsLink.add({ text: 'Hello' }); + + const posts = await commentAutoRemovePostsLink.find().fetchAsync(); + assert.lengthOf(posts, 1); + postId = posts[0]._id; + + assert.isObject(await PostCollection.findOneAsync(postId)); + await CommentCollection.removeAsync(commentId); + assert.isUndefined(await PostCollection.findOneAsync(postId)); + }); + + it('Should set meta link in inversed one-meta', async function () { + const CollectionA = new Mongo.Collection('collectionA' + Random.id()); + const CollectionB = new Mongo.Collection('collectionB' + Random.id()); + + CollectionA.addLinks({ + oneMeta: { + collection: CollectionB, + field: 'oneMetaLink', + type: 'one', + metadata: true, + }, }); - it("Should set meta link in inversed one-meta", function() { - const CollectionA = new Mongo.Collection('collectionA' + Random.id()); - const CollectionB = new Mongo.Collection('collectionB' + Random.id()); - - CollectionA.addLinks({ - oneMeta: { - collection: CollectionB, - field: "oneMetaLink", - type: "one", - metadata: true - } - }); - - CollectionB.addLinks({ - oneMetaA: { - collection: CollectionA, - inversedBy: "oneMeta" - } - }); - - const ADocId = CollectionA.insert({ value: 3 }); - const BDocId = CollectionB.insert({ value: 5 }); - - // console.log({ADocId, BDocId}) - - CollectionB.getLink(BDocId, "oneMetaA").set(ADocId, { - data: "someData" - }); - - const result = CollectionA.createQuery({ - $filters: { _id: ADocId }, - oneMeta: { _id: 1, metadata: 1 } - }).fetchOne(); - - assert.equal('someData', result.oneMeta.$metadata.data); + CollectionB.addLinks({ + oneMetaA: { + collection: CollectionA, + inversedBy: 'oneMeta', + }, }); - it("Should not result in duplicate key error on Many Unique links", function() { - let postIdA = PostCollection.insert({ text: "abc" }); - let postIdB = PostCollection.insert({ text: "abc" }); + const ADocId = await CollectionA.insertAsync({ value: 3 }); + const BDocId = await CollectionB.insertAsync({ value: 5 }); - PostCollection.remove(postIdA); - PostCollection.remove(postIdB); + const link = await CollectionB.getLink(BDocId, 'oneMetaA'); + await link.set(ADocId, { + data: 'someData', }); + const result = await CollectionA.createQuery({ + $filters: { _id: ADocId }, + oneMeta: { _id: 1, metadata: 1 }, + }).fetchOneAsync(); - describe('foreignIdentityField linkConfig param', function () { - beforeEach(function () { - SCDCollection.remove({}); - ReferenceCollection.remove({}); - }); - - it("Works with foreign field - many", function () { - SCDCollection.insert({_id: '1', originalId: '1'}); - SCDCollection.insert({_id: '2', originalId: '1'}); - SCDCollection.insert({_id: '3', originalId: '3'}); - const scd4Id = SCDCollection.insert({_id: '4', originalId: '4'}); + assert.equal('someData', result.oneMeta.$metadata.data); + }); - ReferenceCollection.insert({scdId: '1'}); - ReferenceCollection.insert({scdId: '3'}); - const ref3Id = ReferenceCollection.insert({}); + it('Should not result in duplicate key error on Many Unique links', async function () { + let postIdA = await PostCollection.insertAsync({ text: 'abc' }); + let postIdB = await PostCollection.insertAsync({ text: 'abc' }); - const linkRef = ReferenceCollection.getLink({scdId: '1'}, "scds"); - // both SCDs should be found since they share originalId - assert.lengthOf(linkRef.find().fetch(), 2); + await PostCollection.removeAsync(postIdA); + await PostCollection.removeAsync(postIdB); + }); - const linkSCD = SCDCollection.getLink({_id: '2', originalId: '1'}, "refs"); - assert.lengthOf(linkSCD.find().fetch(), 1); - - // check if it works when links do not exist - const link = ReferenceCollection.getLink(ref3Id, "scds"); - assert.lengthOf(link.find().fetch(), 0); + describe('foreignIdentityField linkConfig param', function () { + beforeEach(async function () { + await SCDCollection.removeAsync({}); + await ReferenceCollection.removeAsync({}); + }); - const inversedLink = SCDCollection.getLink(scd4Id, "refs"); - assert.lengthOf(inversedLink.find().fetch(), 0); - }); + it('Works with foreign field - many', async function () { + await SCDCollection.insertAsync({ _id: '1', originalId: '1' }); + await SCDCollection.insertAsync({ _id: '2', originalId: '1' }); + await SCDCollection.insertAsync({ _id: '3', originalId: '3' }); + const scd4Id = await SCDCollection.insertAsync({ + _id: '4', + originalId: '4', + }); + + await ReferenceCollection.insertAsync({ scdId: '1' }); + await ReferenceCollection.insertAsync({ scdId: '3' }); + const ref3Id = await ReferenceCollection.insertAsync({}); + + const linkRef = await ReferenceCollection.getLink({ scdId: '1' }, 'scds'); + // both SCDs should be found since they share originalId + assert.lengthOf(await linkRef.find().fetchAsync(), 2); + + const linkSCD = await SCDCollection.getLink( + { _id: '2', originalId: '1' }, + 'refs', + ); + assert.lengthOf(await linkSCD.find().fetchAsync(), 1); + + // check if it works when links do not exist + const link = await ReferenceCollection.getLink(ref3Id, 'scds'); + assert.lengthOf(await link.find().fetchAsync(), 0); + + const inversedLink = await SCDCollection.getLink(scd4Id, 'refs'); + assert.lengthOf(await inversedLink.find().fetchAsync(), 0); + }); - it("Auto-removes for foreign field - many", function () { - SCDCollection.insert({_id: '1', originalId: '1'}); - SCDCollection.insert({_id: '2', originalId: '1'}); + it('Auto-removes for foreign field - many', async function () { + await SCDCollection.insertAsync({ _id: '1', originalId: '1' }); + await SCDCollection.insertAsync({ _id: '2', originalId: '1' }); - ReferenceCollection.insert({scdId: '1'}); + await ReferenceCollection.insertAsync({ scdId: '1' }); - // assert.equal(ReferenceCollection.find().count(), 0); - SCDCollection.remove('1'); + // assert.equal(ReferenceCollection.find().count(), 0); + await SCDCollection.removeAsync('1'); - assert.equal(ReferenceCollection.find().count(), 0); - }); + assert.equal(await ReferenceCollection.find().countAsync(), 0); + }); - it("Works with foreign field - one", function () { - SCDCollection.insert({someId: '1'}); + it('Works with foreign field - one', async function () { + await SCDCollection.insertAsync({ someId: '1' }); - ReferenceCollection.insert({some2Id: '1'}); - const ref2Id = ReferenceCollection.insert({some2Id: '2'}); + await ReferenceCollection.insertAsync({ some2Id: '1' }); + const ref2Id = await ReferenceCollection.insertAsync({ some2Id: '2' }); - const linkSCD = SCDCollection.getLink({someId: '1'}, "ref"); - assert.lengthOf(linkSCD.find().fetch(), 1); + const linkSCD = await SCDCollection.getLink({ someId: '1' }, 'ref'); + assert.lengthOf(await linkSCD.find().fetchAsync(), 1); - const linkRef = ReferenceCollection.getLink({some2Id: '1'}, "scd"); - assert.lengthOf(linkRef.find().fetch(), 1); + const linkRef = await ReferenceCollection.getLink( + { some2Id: '1' }, + 'scd', + ); + assert.lengthOf(await linkRef.find().fetchAsync(), 1); - // check if it works when links do not exist - const newId = SCDCollection.insert({}); // no someId - const link = SCDCollection.getLink(newId, "ref"); - assert.lengthOf(link.find().fetch(), 0); + // check if it works when links do not exist + const newId = await SCDCollection.insertAsync({}); // no someId + const link = await SCDCollection.getLink(newId, 'ref'); + assert.lengthOf(await link.find().fetchAsync(), 0); - // inversed - const inversedLink = ReferenceCollection.getLink(ref2Id, "scd"); - assert.lengthOf(inversedLink.find().fetch(), 0); - }); + // inversed + const inversedLink = await ReferenceCollection.getLink(ref2Id, 'scd'); + assert.lengthOf(await inversedLink.find().fetchAsync(), 0); + }); - it("Auto-removes for foreign field - one", function () { - SCDCollection.insert({_id: '1', someId: '1'}); + it('Auto-removes for foreign field - one', async function () { + await SCDCollection.insertAsync({ _id: '1', someId: '1' }); - ReferenceCollection.insert({some2Id: '1'}); - ReferenceCollection.insert({some2Id: '2'}); + await ReferenceCollection.insertAsync({ some2Id: '1' }); + await ReferenceCollection.insertAsync({ some2Id: '2' }); - SCDCollection.remove('1'); + await SCDCollection.removeAsync('1'); - assert.equal(ReferenceCollection.find().count(), 1); - }); + assert.equal(await ReferenceCollection.find().countAsync(), 1); }); + }); }); diff --git a/lib/namedQuery/cache/BaseResultCacher.js b/lib/namedQuery/cache/BaseResultCacher.js index fcbae77c..2145d27a 100644 --- a/lib/namedQuery/cache/BaseResultCacher.js +++ b/lib/namedQuery/cache/BaseResultCacher.js @@ -1,39 +1,52 @@ -import {EJSON} from 'meteor/ejson'; +import { EJSON } from 'meteor/ejson'; /** * This is a very basic in-memory result caching functionality */ export default class BaseResultCacher { - constructor(config = {}) { - this.config = config; - } + constructor(config = {}) { + this.config = config; + } - /** - * @param queryName - * @param params - * @returns {string} - */ - generateQueryId(queryName, params) { - return `${queryName}::${EJSON.stringify(params)}`; - } + /** + * @param queryName + * @param params + * @returns {string} + */ + generateQueryId(queryName, params) { + return `${queryName}::${EJSON.stringify(params)}`; + } + + /** + * Dummy function + */ + fetch(cacheId, { query, countCursor }) { + throw 'Not implemented'; + } - /** - * Dummy function - */ - fetch(cacheId, {query, countCursor}) { - throw 'Not implemented'; + /** + * @param query + * @param countCursor + * @returns {*} + */ + static fetchData({ query, countCursor }) { + if (query) { + return query.fetch(); + } else { + return countCursor.count(); } + } - /** - * @param query - * @param countCursor - * @returns {*} - */ - static fetchData({query, countCursor}) { - if (query) { - return query.fetch(); - } else { - return countCursor.count(); - } + /** + * @param query + * @param countCursor + * @returns {Promise} + */ + static fetchDataAsync({ query, countCursor }) { + if (query) { + return query.fetchAsync(); + } else { + return countCursor.countAsync(); } + } } diff --git a/lib/namedQuery/cache/MemoryResultCacher.js b/lib/namedQuery/cache/MemoryResultCacher.js index ae432ec1..5cb0922f 100644 --- a/lib/namedQuery/cache/MemoryResultCacher.js +++ b/lib/namedQuery/cache/MemoryResultCacher.js @@ -1,4 +1,4 @@ -import {Meteor} from 'meteor/meteor'; +import { Meteor } from 'meteor/meteor'; import cloneDeep from 'lodash.clonedeep'; import BaseResultCacher from './BaseResultCacher'; @@ -8,40 +8,51 @@ const DEFAULT_TTL = 60000; * This is a very basic in-memory result caching functionality */ export default class MemoryResultCacher extends BaseResultCacher { - constructor(config = {}) { - super(config); - this.store = {}; + constructor(config = {}) { + super(config); + this.store = {}; + } + + /** + * @param cacheId + * @param query + * @param countCursor + * @returns {*} + */ + fetch(cacheId, { query, countCursor }) { + const cacheData = this.store[cacheId]; + if (cacheData !== undefined) { + return cloneDeep(cacheData); } - /** - * @param cacheId - * @param query - * @param countCursor - * @returns {*} - */ - fetch(cacheId, {query, countCursor}) { - const cacheData = this.store[cacheId]; - if (cacheData !== undefined) { - return cloneDeep(cacheData); - } - - const data = BaseResultCacher.fetchData({query, countCursor}); - this.storeData(cacheId, data); - - return data; + const data = BaseResultCacher.fetchData({ query, countCursor }); + this.storeData(cacheId, data); + + return data; + } + + async fetchAsync(cacheId, { query, countCursor }) { + const cacheData = this.store[cacheId]; + if (cacheData !== undefined) { + return cloneDeep(cacheData); } + const data = await BaseResultCacher.fetchDataAsync({ query, countCursor }); + this.storeData(cacheId, data); - /** - * @param cacheId - * @param data - */ - storeData(cacheId, data) { - const ttl = this.config.ttl || DEFAULT_TTL; - this.store[cacheId] = cloneDeep(data); + return data; + } - Meteor.setTimeout(() => { - delete this.store[cacheId]; - }, ttl) - } + /** + * @param cacheId + * @param data + */ + storeData(cacheId, data) { + const ttl = this.config.ttl || DEFAULT_TTL; + this.store[cacheId] = cloneDeep(data); + + Meteor.setTimeout(() => { + delete this.store[cacheId]; + }, ttl); + } } diff --git a/lib/namedQuery/expose/extension.js b/lib/namedQuery/expose/extension.js index 5c8fd8a5..ef289a22 100755 --- a/lib/namedQuery/expose/extension.js +++ b/lib/namedQuery/expose/extension.js @@ -8,209 +8,211 @@ import deepClone from 'lodash.clonedeep'; import intersectDeep from '../../query/lib/intersectDeep'; import genCountEndpoint from '../../query/counts/genEndpoint.server'; import { check } from 'meteor/check'; +import { _ } from 'meteor/underscore'; _.extend(NamedQuery.prototype, { - /** - * @param config - */ - expose(config = {}) { - if (!Meteor.isServer) { - throw new Meteor.Error( - 'invalid-environment', - `You must run this in server-side code` - ); - } - - if (this.isExposed) { - throw new Meteor.Error( - 'query-already-exposed', - `You have already exposed: "${this.name}" named query` - ); - } - - this.exposeConfig = Object.assign({}, ExposeDefaults, config); - check(this.exposeConfig, ExposeSchema); - - if (this.exposeConfig.validateParams) { - this.options.validateParams = this.exposeConfig.validateParams; - } - - if (!this.isResolver) { - this._initNormalQuery(); - } else { - this._initMethod(); - } - - this.isExposed = true; - }, - - /** - * Initializes a normal NamedQuery (normal == not a resolver) - * @private - */ - _initNormalQuery() { - const config = this.exposeConfig; - if (config.method) { - this._initMethod(); - } - - if (config.publication) { - this._initPublication(); - } - - if (!config.method && !config.publication) { - throw new Meteor.Error( - 'weird', - 'If you want to expose your named query you need to specify at least one of ["method", "publication"] options to true' - ); - } - - this._initCountMethod(); - this._initCountPublication(); - }, - - /** - * Returns the embodied body of the request - * @param {*} _embody - * @param {*} body - */ - doEmbodimentIfItApplies(body, params) { - // query is not exposed yet, so it doesn't have embodiment logic - if (!this.exposeConfig) { - return; - } - - const { embody } = this.exposeConfig; - - if (!embody) { - return; - } - - if (_.isFunction(embody)) { - embody.call(this, body, params); - } else { - mergeDeep(body, embody); - } - }, - - /** - * @private - */ - _initMethod() { - const self = this; - Meteor.methods({ - [this.name](newParams) { - self._unblockIfNecessary(this); - - // security is done in the fetching because we provide a context - return self.clone(newParams).fetch(this); - }, - }); - }, - - /** - * @returns {void} - * @private - */ - _initCountMethod() { - const self = this; - - Meteor.methods({ - [this.name + '.count'](newParams) { - self._unblockIfNecessary(this); - - // security is done in the fetching because we provide a context - return self.clone(newParams).getCount(this); - }, - }); - }, - - /** - * @returns {*} - * @private - */ - _initCountPublication() { - const self = this; - - genCountEndpoint(self.name, { - getCursor({ session }) { - const query = self.clone(session.params); - return query.getCursorForCounting(); - }, - - getSession(params) { - self.doValidateParams(params); - self._callFirewall(this, this.userId, params); - - return { name: self.name, params, }; - }, - }); - }, - - /** - * @private - */ - _initPublication() { - const self = this; - - Meteor.publishComposite(this.name, function(params = {}) { - const isScoped = !!self.options.scoped; - - if (isScoped) { - this.enableScope(); - } - - self._unblockIfNecessary(this); - self.doValidateParams(params); - self._callFirewall(this, this.userId, params); - - let body = deepClone(self.body); - if (params.$body) { - body = intersectDeep(body, params.$body); - } - - self.doEmbodimentIfItApplies(body, params); - body = prepareForProcess(body, params); - - const rootNode = createGraph(self.collection, body); - - return recursiveCompose(rootNode, undefined, { - scoped: isScoped, - blocking: self.exposeConfig.blocking, - }); - }); - }, - - /** - * @param context - * @param userId - * @param params - * @private - */ - _callFirewall(context, userId, params) { - const { firewall } = this.exposeConfig; - if (!firewall) { - return; - } - - if (Array.isArray(firewall)) { - firewall.forEach(fire => { - fire.call(context, userId, params); - }); - } else { - firewall.call(context, userId, params); - } - }, - - /** - * @param context - * @private - */ - _unblockIfNecessary(context) { - if (this.exposeConfig.unblock) { - if (context.unblock) { - context.unblock(); - } - } - }, + /** + * @this {typeof NamedQuery} + * @param {Grapher.ExposureConfig} [config] + */ + expose(config = {}) { + if (!Meteor.isServer) { + throw new Meteor.Error( + 'invalid-environment', + `You must run this in server-side code`, + ); + } + + if (this.isExposed) { + throw new Meteor.Error( + 'query-already-exposed', + `You have already exposed: "${this.name}" named query`, + ); + } + + this.exposeConfig = Object.assign({}, ExposeDefaults, config); + check(this.exposeConfig, ExposeSchema); + + if (this.exposeConfig.validateParams) { + this.options.validateParams = this.exposeConfig.validateParams; + } + + if (!this.isResolver) { + this._initNormalQuery(); + } else { + this._initMethod(); + } + + this.isExposed = true; + }, + + /** + * Initializes a normal NamedQuery (normal == not a resolver) + * @private + */ + _initNormalQuery() { + const config = this.exposeConfig; + if (config.method) { + this._initMethod(); + } + + if (config.publication) { + this._initPublication(); + } + + if (!config.method && !config.publication) { + throw new Meteor.Error( + 'weird', + 'If you want to expose your named query you need to specify at least one of ["method", "publication"] options to true', + ); + } + + this._initCountMethod(); + this._initCountPublication(); + }, + + /** + * Returns the embodied body of the request + * @param {*} _embody + * @param {*} body + */ + doEmbodimentIfItApplies(body, params) { + // query is not exposed yet, so it doesn't have embodiment logic + if (!this.exposeConfig) { + return; + } + + const { embody } = this.exposeConfig; + + if (!embody) { + return; + } + + if (_.isFunction(embody)) { + embody.call(this, body, params); + } else { + mergeDeep(body, embody); + } + }, + + /** + * @private + */ + _initMethod() { + const self = this; + Meteor.methods({ + [this.name](newParams) { + self._unblockIfNecessary(this); + + // security is done in the fetching because we provide a context + return self.clone(newParams).fetchAsync(this); + }, + }); + }, + + /** + * @returns {void} + * @private + */ + _initCountMethod() { + const self = this; + + Meteor.methods({ + [this.name + '.count'](newParams) { + self._unblockIfNecessary(this); + + // security is done in the fetching because we provide a context + return self.clone(newParams).getCountAsync(this); + }, + }); + }, + + /** + * @returns {*} + * @private + */ + _initCountPublication() { + const self = this; + + genCountEndpoint(self.name, { + getCursor({ session }) { + const query = self.clone(session.params); + return query.getCursorForCounting(); + }, + + async getSession(params) { + self.doValidateParams(params); + await self._callFirewall(this, this.userId, params); + + return { name: self.name, params }; + }, + }); + }, + + /** + * @private + */ + _initPublication() { + const self = this; + + Meteor.publishComposite(this.name, async function (params = {}) { + const isScoped = !!self.options.scoped; + + if (isScoped) { + this.enableScope(); + } + + self._unblockIfNecessary(this); + await self.doValidateParams(params); + await self._callFirewall(this, this.userId, params); + + let body = deepClone(self.body); + if (params.$body) { + body = intersectDeep(body, params.$body); + } + + self.doEmbodimentIfItApplies(body, params); + body = prepareForProcess(body, params); + + const rootNode = createGraph(self.collection, body); + + return recursiveCompose(rootNode, undefined, { + scoped: isScoped, + blocking: self.exposeConfig.blocking, + }); + }); + }, + + /** + * @param context + * @param userId + * @param params + * @private + */ + async _callFirewall(context, userId, params) { + const { firewall } = this.exposeConfig; + if (!firewall) { + return; + } + + if (Array.isArray(firewall)) { + for (const f of firewall) { + await f.call(context, userId, params); + } + } else { + await firewall.call(context, userId, params); + } + }, + + /** + * @param context + * @private + */ + _unblockIfNecessary(context) { + if (this.exposeConfig.unblock) { + if (context.unblock) { + context.unblock(); + } + } + }, }); diff --git a/lib/namedQuery/namedQuery.base.js b/lib/namedQuery/namedQuery.base.js index 5685365c..d4b643b8 100755 --- a/lib/namedQuery/namedQuery.base.js +++ b/lib/namedQuery/namedQuery.base.js @@ -1,98 +1,157 @@ import deepClone from 'lodash.clonedeep'; +import { check } from 'meteor/check'; +import { _ } from 'meteor/underscore'; +/** + * @type {Partial} + */ let globalConfig = {}; +/** + * @template T + * @template {Grapher.Params} P Params type + * + */ export default class NamedQueryBase { - static setConfig(config) { - globalConfig = config; + /** + * @param {Partial} config + */ + static setConfig(config) { + globalConfig = config; + } + + static getConfig() { + return globalConfig; + } + + isNamedQuery = true; + + /** + * @param {string} name + * @param {Mongo.Collection} collection + * @param {Grapher.Body} body + * @param {Grapher.QueryOptions} options + */ + constructor(name, collection, body, options = {}) { + this.queryName = name; + + if (_.isFunction(body)) { + this.resolver = body; + } else { + this.body = deepClone(body); } - static getConfig() { - return globalConfig; - } - - isNamedQuery = true; - - constructor(name, collection, body, options = {}) { - this.queryName = name; - - if (_.isFunction(body)) { - this.resolver = body; - } else { - this.body = deepClone(body); - } - - this.subscriptionHandle = null; - this.params = options.params || {}; - this.options = Object.assign({}, globalConfig, options); - this.collection = collection; - this.isExposed = false; - } - - get name() { - return `named_query_${this.queryName}`; - } - - get isResolver() { - return !!this.resolver; - } - - setParams(params) { - this.params = _.extend({}, this.params, params); + /** + * @type {Meteor.SubscriptionHandle | null} + */ + this.subscriptionHandle = null; + /** + * @type {P} + */ + this.params = options.params || {}; + /** + * @type {Grapher.QueryOptions} + */ + this.options = Object.assign({}, globalConfig, options); - return this; - } + /** + * @type {Mongo.Collection} + */ + this.collection = collection; /** - * Validates the parameters + * @type {boolean} */ - doValidateParams(params) { - params = params || this.params; - - const {validateParams} = this.options; - if (!validateParams) return; - - try { - this._validate(validateParams, params); - } catch (validationError) { - console.error(`Invalid parameters supplied to the query "${this.queryName}"\n`, validationError); - throw validationError; // rethrow - } - } + this.isExposed = false; - clone(newParams) { - const params = _.extend({}, deepClone(this.params), newParams); - - let clone = new this.constructor( - this.queryName, - this.collection, - this.isResolver ? this.resolver : deepClone(this.body), - { - ...this.options, - params, - } - ); - - clone.cacher = this.cacher; - if (this.exposeConfig) { - clone.exposeConfig = this.exposeConfig; - } - - return clone; + /** + * Server only + * @type {Grapher.ExposureConfig | null} + */ + this.exposeConfig = null; + } + + get name() { + return `named_query_${this.queryName}`; + } + + get isResolver() { + return !!this.resolver; + } + + /** + * @param {P} params + * @returns + */ + setParams(params) { + this.params = _.extend({}, this.params, params); + + return this; + } + + /** + * Validates the parameters + * @param {P} params + * + * @returns {void} + */ + doValidateParams(params) { + params = params || this.params; + + const { validateParams } = this.options; + if (!validateParams) return; + + try { + this._validate(validateParams, params); + } catch (validationError) { + console.error( + `Invalid parameters supplied to the query "${this.queryName}"\n`, + validationError, + ); + throw validationError; // rethrow } + } + /** + * @param {P} newParams + * @returns + */ + clone(newParams) { + const params = _.extend({}, deepClone(this.params), newParams); /** - * @param {function|object} validator - * @param {object} params - * @private + * @type {Grapher.NamedQueryBaseClass} */ - _validate(validator, params) { - if (_.isFunction(validator)) { - validator.call(null, params) - } else { - check(params, validator) - } + let clone = new this.constructor( + this.queryName, + this.collection, + this.isResolver ? this.resolver : deepClone(this.body), + { + ...this.options, + params, + }, + ); + + clone.cacher = this.cacher; + if (this.exposeConfig) { + clone.exposeConfig = this.exposeConfig; + } + + return clone; + } + + /** + * @param {Grapher.ValidateParamsParam} validator + * @param {P} params + * @returns {void} + * @private + */ + _validate(validator, params) { + if (typeof validator === 'function') { + validator.call(null, params); + } else { + check(params, validator); } + } } -NamedQueryBase.defaultOptions = {}; \ No newline at end of file +NamedQueryBase.defaultOptions = {}; diff --git a/lib/namedQuery/namedQuery.client.js b/lib/namedQuery/namedQuery.client.js index 5c8eeb95..1625a7ac 100755 --- a/lib/namedQuery/namedQuery.client.js +++ b/lib/namedQuery/namedQuery.client.js @@ -2,189 +2,222 @@ import CountSubscription from '../query/counts/countSubscription'; import createGraph from '../query/lib/createGraph.js'; import recursiveFetch from '../query/lib/recursiveFetch.js'; import prepareForProcess from '../query/lib/prepareForProcess.js'; -import {_} from 'meteor/underscore'; -import callWithPromise from '../query/lib/callWithPromise'; +import { _ } from 'meteor/underscore'; import Base from './namedQuery.base'; -import {LocalCollection} from 'meteor/minimongo'; import intersectDeep from '../query/lib/intersectDeep'; +/** + * @template T + * @template {Grapher.Params} P Params type + * @extends Base + */ export default class extends Base { - /** - * Subscribe - * - * @param callback - * @returns {null|any|*} - */ - subscribe(callback) { - if (this.isResolver) { - throw new Meteor.Error('not-allowed', `You cannot subscribe to a resolver query`); - } - - this.subscriptionHandle = Meteor.subscribe( - this.name, - this.params, - callback - ); - - return this.subscriptionHandle; + /** + * Subscribe + * + * @param {Grapher.MeteorSubscribeCallbacks} callback + * @returns {Meteor.SubscriptionHandle} + */ + subscribe(callback) { + if (this.isResolver) { + throw new Meteor.Error( + 'not-allowed', + `You cannot subscribe to a resolver query`, + ); } - /** - * Subscribe to the counts for this query - * - * @param callback - * @returns {Object} - */ - subscribeCount(callback) { - if (this.isResolver) { - throw new Meteor.Error('not-allowed', `You cannot subscribe to a resolver query`); - } - - if (!this._counter) { - this._counter = new CountSubscription(this); - } - - return this._counter.subscribe(this.params, callback); + const subscriptionHandle = Meteor.subscribe( + this.name, + this.params, + callback, + ); + + this.subscriptionHandle = subscriptionHandle; + + return subscriptionHandle; + } + + /** + * Subscribe to the counts for this query + * + * @param {Grapher.MeteorSubscribeCallbacks} callback + * @returns {Object} + */ + subscribeCount(callback) { + if (this.isResolver) { + throw new Meteor.Error( + 'not-allowed', + `You cannot subscribe to a resolver query`, + ); } - /** - * Unsubscribe if an existing subscription exists - */ - unsubscribe() { - if (this.subscriptionHandle) { - this.subscriptionHandle.stop(); - } - - this.subscriptionHandle = null; + if (!this._counter) { + this._counter = new CountSubscription(this); } - /** - * Unsubscribe to the counts if a subscription exists. - */ - unsubscribeCount() { - if (this._counter) { - this._counter.unsubscribe(); - this._counter = null; - } + return this._counter.subscribe(this.params, callback); + } + + /** + * Unsubscribe if an existing subscription exists + */ + unsubscribe() { + if (this.subscriptionHandle) { + this.subscriptionHandle.stop(); } - /** - * Fetches elements in sync using promises - * @return {*} - */ - async fetchSync() { - if (this.subscriptionHandle) { - throw new Meteor.Error('This query is reactive, meaning you cannot use promises to fetch the data.'); - } + this.subscriptionHandle = null; + } - return await callWithPromise(this.name, this.params); + /** + * Unsubscribe to the counts if a subscription exists. + */ + unsubscribeCount() { + if (this._counter) { + this._counter.unsubscribe(); + this._counter = null; } - - /** - * Fetches one element in sync - * @return {*} - */ - async fetchOneSync() { - return _.first(await this.fetchSync()) + } + + /** + * @deprecated + * @return {Promise} + */ + async fetchSync() { + return this.fetchAsync(); + } + + /** + * @deprecated + * + * @returns Promise + */ + fetchOneSync() { + return this.fetchOneAsync(); + } + + /** + * Fetches one element in sync + * @return {Promise} + */ + async fetchOneAsync() { + return _.first(await this.fetchAsync()); + } + + /** + * Retrieves the data. + * @param callbackOrOptions + * @returns {*} + */ + fetch(callbackOrOptions) { + if (!this.subscriptionHandle) { + throw new Error( + 'Please use fetchAsync instead of fetch outside of subscription', + ); + } else { + return this._fetchReactive(callbackOrOptions); } + } - /** - * Retrieves the data. - * @param callbackOrOptions - * @returns {*} - */ - fetch(callbackOrOptions) { - if (!this.subscriptionHandle) { - return this._fetchStatic(callbackOrOptions) - } else { - return this._fetchReactive(callbackOrOptions); - } + fetchAsync() { + if (this.subscriptionHandle) { + throw new Meteor.Error( + 'This query is reactive, meaning you cannot use promises to fetch the data.', + ); } - - /** - * @param args - * @returns {*} - */ - fetchOne(...args) { - if (!this.subscriptionHandle) { - const callback = args[0]; - if (!_.isFunction(callback)) { - throw new Meteor.Error('You did not provide a valid callback'); - } - - this.fetch((err, res) => { - callback(err, res ? _.first(res) : null); - }) - } else { - return _.first(this.fetch(...args)); - } + return this._fetchStatic(); + } + + /** + * @param args + * @returns {*} + */ + fetchOne(...args) { + if (!this.subscriptionHandle) { + const callback = args[0]; + if (!_.isFunction(callback)) { + throw new Meteor.Error('You did not provide a valid callback'); + } + + this.fetch((err, res) => { + callback(err, res ? _.first(res) : null); + }); + } else { + return _.first(this.fetch(...args)); } - - /** - * Gets the count of matching elements in sync. - * @returns {any} - */ - async getCountSync() { - if (this._counter) { - throw new Meteor.Error('This query is reactive, meaning you cannot use promises to fetch the data.'); - } - - return await callWithPromise(this.name + '.count', this.params); + } + + /** + * @deprecated Use getCountAsync + */ + async getCountSync() { + return this.getCountAsync(); + } + + /** + * Gets the count of matching elements. + * @returns {any} + */ + getCount() { + if (this._counter) { + return this._counter.getCount(); + } else { + throw new Error( + 'Please use getCountAsync instead of getCount for static queries', + ); + // if (!callback) { + // throw new Meteor.Error( + // 'not-allowed', + // 'You are on client so you must either provide a callback to get the count or subscribe first.', + // ); + // } else { + // return Meteor.call(this.name + '.count', this.params, callback); + // } } - - /** - * Gets the count of matching elements. - * @param callback - * @returns {any} - */ - getCount(callback) { - if (this._counter) { - return this._counter.getCount(); - } else { - if (!callback) { - throw new Meteor.Error('not-allowed', 'You are on client so you must either provide a callback to get the count or subscribe first.'); - } else { - return Meteor.call(this.name + '.count', this.params, callback); - } - } + } + + /** + * Gets the count of matching elements in sync. + * @returns {Promise} + */ + getCountAsync() { + if (this._counter) { + throw new Meteor.Error( + 'This query is reactive, meaning you cannot use promises to fetch the data.', + ); } - - /** - * Fetching non-reactive queries - * @param callback - * @private - */ - _fetchStatic(callback) { - if (!callback) { - throw new Meteor.Error('not-allowed', 'You are on client so you must either provide a callback to get the data or subscribe first.'); - } - - Meteor.call(this.name, this.params, callback); + return Meteor.callAsync(this.name + '.count', this.params); + } + + /** + * Fetching non-reactive queries + * @private + */ + _fetchStatic() { + return Meteor.callAsync(this.name, this.params); + } + + /** + * Fetching when we've got an active publication + * + * @param options + * @returns {*} + * @private + */ + _fetchReactive(options = {}) { + let body = this.body; + if (this.params.$body) { + body = intersectDeep(body, this.params.$body); } - /** - * Fetching when we've got an active publication - * - * @param options - * @returns {*} - * @private - */ - _fetchReactive(options = {}) { - let body = this.body; - if (this.params.$body) { - body = intersectDeep(body, this.params.$body); - } - - body = prepareForProcess(body, this.params); - if (!options.allowSkip && body.$options && body.$options.skip) { - delete body.$options.skip; - } - - return recursiveFetch( - createGraph(this.collection, body), - undefined, { - scoped: this.options.scoped, - subscriptionHandle: this.subscriptionHandle - }); + body = prepareForProcess(body, this.params); + if (!options.allowSkip && body.$options && body.$options.skip) { + delete body.$options.skip; } + + return recursiveFetch(createGraph(this.collection, body), undefined, { + scoped: this.options.scoped, + subscriptionHandle: this.subscriptionHandle, + }); + } } diff --git a/lib/namedQuery/namedQuery.js b/lib/namedQuery/namedQuery.js index 42c8de86..9b120b78 100644 --- a/lib/namedQuery/namedQuery.js +++ b/lib/namedQuery/namedQuery.js @@ -1,12 +1,15 @@ import NamedQueryClient from './namedQuery.client'; import NamedQueryServer from './namedQuery.server'; +/** + * @type {typeof NamedQueryClient | typeof NamedQueryServer} + */ let NamedQuery; if (Meteor.isServer) { - NamedQuery = NamedQueryServer; + NamedQuery = NamedQueryServer; } else { - NamedQuery = NamedQueryClient; + NamedQuery = NamedQueryClient; } -export default NamedQuery; \ No newline at end of file +export default NamedQuery; diff --git a/lib/namedQuery/namedQuery.server.js b/lib/namedQuery/namedQuery.server.js index 637a28fe..faa7524a 100644 --- a/lib/namedQuery/namedQuery.server.js +++ b/lib/namedQuery/namedQuery.server.js @@ -3,136 +3,169 @@ import Base from './namedQuery.base'; import deepClone from 'lodash.clonedeep'; import MemoryResultCacher from './cache/MemoryResultCacher'; import intersectDeep from '../query/lib/intersectDeep'; - +import { _ } from 'meteor/underscore'; +import { Meteor } from 'meteor/meteor'; + +/** + * @template T + * @template {Grapher.Params} P Params type + * @extends Base + */ export default class extends Base { - /** - * Retrieves the data. - * @returns {*} - */ - fetch(context) { - this._performSecurityChecks(context, this.params); - - if (this.isResolver) { - return this._fetchResolverData(context); - } else { - body = deepClone(this.body); - if (this.params.$body) { - body = intersectDeep(body, this.params.$body); - } - - // we must apply emobdyment here - this.doEmbodimentIfItApplies(body, this.params); - - const query = this.collection.createQuery( - deepClone(body), - { - params: deepClone(this.params) - } - ); - - if (this.cacher) { - const cacheId = this.cacher.generateQueryId(this.queryName, this.params); - return this.cacher.fetch(cacheId, {query}); - } - - return query.fetch(); - } - } - - /** - * @param args - * @returns {*} - */ - fetchOne(...args) { - return _.first(this.fetch(...args)); + /** + * Retrieves the data. + * + * @param {Grapher.QueryFetchContext} [context] + * @returns {*} + */ + fetch(context) { + throw new Error('fetch is not available on server, please use fetchAsync'); + } + + /** + * + * @param {Grapher.QueryFetchContext} [context] + */ + async fetchAsync(context) { + await this._performSecurityChecks(context, this.params); + + if (this.isResolver) { + return this._fetchResolverData(context); + } else { + let body = deepClone(this.body); + if (this.params.$body) { + body = intersectDeep(body, this.params.$body); + } + + // we must apply emobdyment here + this.doEmbodimentIfItApplies(body, this.params); + + const query = this.collection.createQuery(deepClone(body), { + params: deepClone(this.params), + }); + + if (this.cacher) { + const cacheId = this.cacher.generateQueryId( + this.queryName, + this.params, + ); + return this.cacher.fetchAsync(cacheId, { query }); + } + + return query.fetchAsync(); } - - /** - * Gets the count of matching elements. - * - * @returns {any} - */ - getCount(context) { - this._performSecurityChecks(context, this.params); - - const countCursor = this.getCursorForCounting(); - - if (this.cacher) { - const cacheId = 'count::' + this.cacher.generateQueryId(this.queryName, this.params); - - return this.cacher.fetch(cacheId, {countCursor}); - } - - return countCursor.count(); + } + + /** + * @param args + * @returns {*} + */ + fetchOne(...args) { + return _.first(this.fetch(...args)); + } + + /** + * Gets the count of matching elements. + * + * @returns {any} + */ + getCount(context) { + throw new Error('count is not available on server, please use countAsync'); + } + + /** + * Gets the count of matching elements. + * + * @param {Grapher.QueryFetchContext} [context] + * @returns {Promise} + */ + async getCountAsync(context) { + await this._performSecurityChecks(context, this.params); + + const countCursor = this.getCursorForCounting(); + + if (this.cacher) { + const cacheId = + 'count::' + this.cacher.generateQueryId(this.queryName, this.params); + + return this.cacher.fetchAsync(cacheId, { countCursor }); } - /** - * Returns the cursor for counting - * This is most likely used for counts cursor - */ - getCursorForCounting() { - let body = deepClone(this.body); - this.doEmbodimentIfItApplies(body, this.params); - body = prepareForProcess(body, this.params); - - return this.collection.find(body.$filters || {}, {fields: {_id: 1}}); + return countCursor.countAsync(); + } + + /** + * Returns the cursor for counting + * This is most likely used for counts cursor + */ + getCursorForCounting() { + let body = deepClone(this.body); + this.doEmbodimentIfItApplies(body, this.params); + body = prepareForProcess(body, this.params); + + return this.collection.find(body.$filters || {}, { fields: { _id: 1 } }); + } + + /** + * @param cacher + */ + cacheResults(cacher) { + if (!cacher) { + cacher = new MemoryResultCacher(); } - /** - * @param cacher - */ - cacheResults(cacher) { - if (!cacher) { - cacher = new MemoryResultCacher(); - } - - this.cacher = cacher; + this.cacher = cacher; + } + + /** + * Configure resolve. This doesn't actually call the resolver, it just sets it + * @param fn + */ + resolve(fn) { + if (!this.isResolver) { + throw new Meteor.Error( + 'invalid-call', + `You cannot use resolve() on a non resolver NamedQuery`, + ); } - /** - * Configure resolve. This doesn't actually call the resolver, it just sets it - * @param fn - */ - resolve(fn) { - if (!this.isResolver) { - throw new Meteor.Error('invalid-call', `You cannot use resolve() on a non resolver NamedQuery`); - } - - this.resolver = fn; + this.resolver = fn; + } + + /** + * @returns {*} + * @private + */ + _fetchResolverData(context) { + const resolver = this.resolver; + const self = this; + const query = { + fetch() { + return resolver.call(context, self.params); + }, + }; + + if (this.cacher) { + const cacheId = this.cacher.generateQueryId(this.queryName, this.params); + return this.cacher.fetch(cacheId, { query }); } - /** - * @returns {*} - * @private - */ - _fetchResolverData(context) { - const resolver = this.resolver; - const self = this; - const query = { - fetch() { - return resolver.call(context, self.params); - } - }; - - if (this.cacher) { - const cacheId = this.cacher.generateQueryId(this.queryName, this.params); - return this.cacher.fetch(cacheId, {query}); - } - - return query.fetch(); + return query.fetch(); + } + + /** + * @param {Grapher.QueryFetchContext} [context] Meteor method/publish context + * @param {P} [params] + * + * @returns {Promise} + * + * @private + */ + async _performSecurityChecks(context, params) { + if (context && this.exposeConfig) { + await this._callFirewall(context, context.userId, params); } - /** - * @param context Meteor method/publish context - * @param params - * - * @private - */ - _performSecurityChecks(context, params) { - if (context && this.exposeConfig) { - this._callFirewall(context, context.userId, params); - } - - this.doValidateParams(params); - } + this.doValidateParams(params); + } } diff --git a/lib/namedQuery/testing/bootstrap/queries/postListExposure.js b/lib/namedQuery/testing/bootstrap/queries/postListExposure.js index e663af04..66e00548 100644 --- a/lib/namedQuery/testing/bootstrap/queries/postListExposure.js +++ b/lib/namedQuery/testing/bootstrap/queries/postListExposure.js @@ -1,39 +1,41 @@ import { createQuery } from 'meteor/cultofcoders:grapher'; const postListExposure = createQuery('postListExposure', { - posts: { - title: 1, - author: { - name: 1 - }, - group: { - name: 1 - } - } + posts: { + title: 1, + author: { + name: 1, + }, + group: { + name: 1, + }, + }, }); -export const postListFilteredWithDate = createQuery('postListFilteredWithDate', { +export const postListFilteredWithDate = createQuery( + 'postListFilteredWithDate', + { posts: { - $filter({filters, options, params}) { - if (params.date) { - filters.createdAt = {$lte: params.date} - } - }, - createdAt:1, - } -}); + $filter({ filters, options, params }) { + if (params.date) { + filters.createdAt = { $lte: params.date }; + } + }, + createdAt: 1, + }, + }, +); if (Meteor.isServer) { - postListExposure.expose({ - firewall(userId, params) { - }, - embody: { - $filter({filters, params}) { - filters.title = params.title - } - } - }); - postListFilteredWithDate.expose({}); + postListExposure.expose({ + firewall(userId, params) {}, + embody: { + $filter({ filters, params }) { + filters.title = params.title; + }, + }, + }); + postListFilteredWithDate.expose({}); } -export default postListExposure; \ No newline at end of file +export default postListExposure; diff --git a/lib/namedQuery/testing/bootstrap/queries/productsList.js b/lib/namedQuery/testing/bootstrap/queries/productsList.js index 4e44378b..ca00bba4 100644 --- a/lib/namedQuery/testing/bootstrap/queries/productsList.js +++ b/lib/namedQuery/testing/bootstrap/queries/productsList.js @@ -1,23 +1,24 @@ -import { Products } from "../../../../query/testing/bootstrap/products/collection"; +import { Products } from '../../../../query/testing/bootstrap/products/collection'; const productsList = Products.createQuery('productsList', { - title: 1, - price: 1, + title: 1, + price: 1, + productId: 1, + attributes: { productId: 1, - attributes: { - productId: 1, - unit: 1, - delivery: 1, - }, - singleAttribute: { - delivery: 1, - }, + unit: 1, + delivery: 1, + }, + singleAttribute: { + delivery: 1, + }, + ordering: 1, }); if (Meteor.isServer) { - productsList.expose({ - firewall() {}, - }); + productsList.expose({ + firewall() {}, + }); } export default productsList; diff --git a/lib/namedQuery/testing/client.test.js b/lib/namedQuery/testing/client.test.js index 076437f9..80c27858 100755 --- a/lib/namedQuery/testing/client.test.js +++ b/lib/namedQuery/testing/client.test.js @@ -1,331 +1,349 @@ import { assert, expect } from 'chai'; -import postListExposure, {postListFilteredWithDate} from './bootstrap/queries/postListExposure.js'; +import postListExposure, { + postListFilteredWithDate, +} from './bootstrap/queries/postListExposure.js'; import postListExposureScoped from './bootstrap/queries/postListExposureScoped'; import userListScoped from './bootstrap/queries/userListScoped'; import productsList from './bootstrap/queries/productsList'; import { createQuery } from 'meteor/cultofcoders:grapher'; import Posts from '../../query/testing/bootstrap/posts/collection'; import Users from '../../query/testing/bootstrap/users/collection'; +import { _ } from 'meteor/underscore'; describe('Named Query', function () { - it('Should return proper values', function (done) { - const query = createQuery({ - postListExposure: { - title: 'User Post - 3', - }, - }); + it('Should return proper values', async function () { + const query = createQuery({ + postListExposure: { + title: 'User Post - 3', + }, + }); - query.fetch((err, res) => { - assert.isUndefined(err); - assert.isTrue(res.length > 0); + const res = await query.fetchAsync(); - _.each(res, post => { - assert.equal(post.title, 'User Post - 3'); - assert.isObject(post.author); - assert.isObject(post.group); - }); + assert.isTrue(res.length > 0); - done(); - }); + _.each(res, (post) => { + assert.equal(post.title, 'User Post - 3'); + assert.isObject(post.author); + assert.isObject(post.group); }); + }); - it('Should return proper values using query directly via import', function (done) { - const query = postListExposure.clone({ title: 'User Post - 3' }); + it('Should return proper values using query directly via import', async function () { + const query = postListExposure.clone({ title: 'User Post - 3' }); - query.fetch((err, res) => { - assert.isUndefined(err); - assert.isTrue(res.length > 0); + const res = await query.fetchAsync(); - _.each(res, post => { - assert.equal(post.title, 'User Post - 3'); - assert.isObject(post.author); - assert.isObject(post.group); - }); + assert.isTrue(res.length > 0); - done(); - }); + _.each(res, (post) => { + assert.equal(post.title, 'User Post - 3'); + assert.isObject(post.author); + assert.isObject(post.group); }); + }); - it('Should return proper values using query directly via import - sync', async function () { - const query = postListExposure.clone({ title: 'User Post - 3' }); + it('Should return proper values using query directly via import - sync', async function () { + const query = postListExposure.clone({ title: 'User Post - 3' }); - const res = await query.fetchSync(); + const res = await query.fetchSync(); - assert.isTrue(res.length > 0); + assert.isTrue(res.length > 0); - _.each(res, post => { - assert.equal(post.title, 'User Post - 3'); - assert.isObject(post.author); - assert.isObject(post.group); - }); + _.each(res, (post) => { + assert.equal(post.title, 'User Post - 3'); + assert.isObject(post.author); + assert.isObject(post.group); }); + }); - it('Should work with count', function (done) { - const query = postListExposure.clone({ title: 'User Post - 3' }); + it('Should work with count', async function () { + const query = postListExposure.clone({ title: 'User Post - 3' }); - query.getCount((err, res) => { - assert.equal(6, res); - done(); - }); - }); + const res = await query.getCountAsync(); + assert.equal(6, res); + }); - it('Should work with count when filtering on dates', function(done) { - const query = postListFilteredWithDate.clone({date: new Date()}); + it('Should work with count when filtering on dates', async function () { + const query = postListFilteredWithDate.clone({ date: new Date() }); - query.getCount((err, res) => { - assert.equal(36, res); - done(); - }); - }); + const res = await query.getCountAsync(); + assert.equal(36, res); + }); - it('Should work with count - sync', async function () { - const query = postListExposure.clone({ title: 'User Post - 3' }); + it('Should work with count - sync', async function () { + const query = postListExposure.clone({ title: 'User Post - 3' }); - const count = await query.getCountSync(); - assert.equal(6, count); - }); + const count = await query.getCountSync(); + assert.equal(6, count); + }); - it('Should work with count - sync when filtering on dates', async function() { - const query = postListFilteredWithDate.clone({date: new Date()}); + it('Should work with count - sync when filtering on dates', async function () { + const query = postListFilteredWithDate.clone({ date: new Date() }); - const count = await query.getCountSync(); - assert.equal(36, count); - }); + const count = await query.getCountSync(); + assert.equal(36, count); + }); - it('Should work with reactive counts', function (done) { - const query = postListExposure.clone({ title: 'User Post - 3' }); + it('Should work with reactive counts', function (done) { + const query = postListExposure.clone({ title: 'User Post - 3' }); - const handle = query.subscribeCount(); - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const count = query.getCount(); - handle.stop(); + const handle = query.subscribeCount(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + const count = query.getCount(); + handle.stop(); - assert.equal(count, 6); - done(); - } - }); + assert.equal(count, 6); + done(); + } }); - it('Should work with reactive counts when filtering ondates', function(done) { - const query = postListFilteredWithDate.clone({date: new Date()}); - - const handle = query.subscribe(); - const handleCount = query.subscribeCount(); - Tracker.autorun(c => { - if (handle.ready() && handleCount.ready()) { - c.stop(); - const count = query.getCount(); - const data = query.fetch(); - handle.stop(); - handleCount.stop(); - assert.equal(data.length, 36); - assert.equal(count, 36); - done(); - } - }); + }); + it('Should work with reactive counts when filtering on dates', function (done) { + const query = postListFilteredWithDate.clone({ date: new Date() }); + + const handle = query.subscribe(); + const handleCount = query.subscribeCount(); + Tracker.autorun((c) => { + if (handle.ready() && handleCount.ready()) { + try { + c.stop(); + const count = query.getCount(); + const data = query.fetch(); + handle.stop(); + handleCount.stop(); + + assert.equal(data.length, 36); + assert.equal(count, 36); + done(); + } catch (err) { + // done(err); + } + } }); + }); - it('Should work with reactive queries', function (done) { - const query = createQuery({ - postListExposure: { - title: 'User Post - 3', - }, - }); - - const handle = query.subscribe(); + it('Should work with reactive queries', function (done) { + const query = createQuery({ + postListExposure: { + title: 'User Post - 3', + }, + }); - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const res = query.fetch(); - handle.stop(); + const handle = query.subscribe(); - assert.isTrue(res.length > 0); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + const res = query.fetch(); + handle.stop(); - _.each(res, post => { - assert.equal(post.title, 'User Post - 3'); - assert.isObject(post.author); - assert.isObject(post.group); - }); + assert.isTrue(res.length > 0); - done(); - } + _.each(res, (post) => { + assert.equal(post.title, 'User Post - 3'); + assert.isObject(post.author); + assert.isObject(post.group); }); - }); - it('Should work with reactive scoped queries', function (done) { - const query = postListExposureScoped.clone({ title: 'User Post - 3' }); - - const handle = query.subscribe(); - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const data = query.fetch(); - handle.stop(); - - assert.isTrue(data.length > 0); - - // swap this over to an object - // since in 2.5+ an actual Map is used - const postDocs = Posts._collection._docs._map; - const docMap = postDocs instanceof Map ? Object.fromEntries(postDocs) : postDocs; - - const scopeField = `_sub_${handle.subscriptionId}`; - const queryPathField = '_query_path_posts'; - data.forEach(post => { - // no scope field returned from find - assert.isUndefined(post[scopeField]); - assert.isObject(docMap[post._id]); - assert.equal(docMap[post._id][scopeField], 1); - assert.equal(docMap[post._id][queryPathField], 1); - }); - - done(); - } - }); + done(); + } }); - - it('Should work with reactive recursive scoped queries', function (done) { - const query = userListScoped.clone({ name: 'User - 3' }); - - const handle = query.subscribe(); - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const data = query.fetch(); - handle.stop(); - - assert.equal(data.length, 1); - // User 3 has users 0,1,2 as friends and user 2 as subordinate - const [user3] = data; - assert.equal(user3.friends.length, 3); - - // swap this over to an object - // since in 2.5+ an actual Map is used - const userDocs = Users._collection._docs._map; - const docMap = userDocs instanceof Map ? Object.fromEntries(userDocs) : userDocs; - // users collection on the client should have 4 items (user 3 and friends - user 0,1,2) - assert.equal(_.keys(docMap).length, 4); - - const scopeField = `_sub_${handle.subscriptionId}`; - const rootQueryPathField = '_query_path_users'; - const friendsQueryPathField = '_query_path_users_friends'; - const adversaryQueryPathField = '_query_path_users_subordinates'; - Object.entries(docMap).forEach(([userId, userDoc]) => { - const isRoot = userId === user3._id; - assert.equal(userDoc[scopeField], 1); - if (isRoot) { - assert.equal(userDoc[rootQueryPathField], 1); - assert.isTrue(!(friendsQueryPathField in userDoc)); - assert.isTrue(!(adversaryQueryPathField in userDoc)); - } - else { - assert.equal(userDoc[friendsQueryPathField], 1); - assert.isTrue(!(rootQueryPathField in userDoc)); - - if (userDoc.name === 'User - 2') { - assert.equal(userDoc[adversaryQueryPathField], 1); - } - else { - assert.isTrue(!(adversaryQueryPathField in userDoc)); - } - } - }); - - done(); - } - }); + }); + + it('Should work with reactive scoped queries', function (done) { + const query = postListExposureScoped.clone({ title: 'User Post - 3' }); + + const handle = query.subscribe(); + Tracker.autorun((c) => { + if (handle.ready()) { + try { + c.stop(); + const data = query.fetch(); + handle.stop(); + + assert.isTrue(data.length > 0); + + // swap this over to an object + // since in 2.5+ an actual Map is used + const postDocs = Posts._collection._docs._map; + const docMap = + postDocs instanceof Map ? Object.fromEntries(postDocs) : postDocs; + + const scopeField = `_sub_${handle.subscriptionId}`; + const queryPathField = '_query_path_posts'; + data.forEach((post) => { + // no scope field returned from find + assert.isUndefined(post[scopeField]); + assert.isObject(docMap[post._id]); + assert.equal(docMap[post._id][scopeField], 1); + assert.equal(docMap[post._id][queryPathField], 1); + }); + + done(); + } catch (e) { + done(e); + } + } }); - - it('Should work with reactive queries via import', function (done) { - const query = postListExposure.clone({ - title: 'User Post - 3', + }); + + it('Should work with reactive recursive scoped queries', function (done) { + const query = userListScoped.clone({ name: 'User - 3' }); + + const handle = query.subscribe(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + const data = query.fetch(); + handle.stop(); + + assert.equal(data.length, 1); + // User 3 has users 0,1,2 as friends and user 2 as subordinate + const [user3] = data; + assert.equal(user3.friends.length, 3); + + // swap this over to an object + // since in 2.5+ an actual Map is used + const userDocs = Users._collection._docs._map; + const docMap = + userDocs instanceof Map ? Object.fromEntries(userDocs) : userDocs; + // users collection on the client should have 4 items (user 3 and friends - user 0,1,2) + assert.equal(_.keys(docMap).length, 4); + + const scopeField = `_sub_${handle.subscriptionId}`; + const rootQueryPathField = '_query_path_users'; + const friendsQueryPathField = '_query_path_users_friends'; + const adversaryQueryPathField = '_query_path_users_subordinates'; + Object.entries(docMap).forEach(([userId, userDoc]) => { + const isRoot = userId === user3._id; + assert.equal(userDoc[scopeField], 1); + if (isRoot) { + assert.equal(userDoc[rootQueryPathField], 1); + assert.isTrue(!(friendsQueryPathField in userDoc)); + assert.isTrue(!(adversaryQueryPathField in userDoc)); + } else { + assert.equal(userDoc[friendsQueryPathField], 1); + assert.isTrue(!(rootQueryPathField in userDoc)); + + if (userDoc.name === 'User - 2') { + assert.equal(userDoc[adversaryQueryPathField], 1); + } else { + assert.isTrue(!(adversaryQueryPathField in userDoc)); + } + } }); - const handle = query.subscribe(); - - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const res = query.fetch(); - handle.stop(); - - assert.isTrue(res.length > 0); - - _.each(res, post => { - assert.equal(post.title, 'User Post - 3'); - assert.isObject(post.author); - assert.isObject(post.group); - }); + done(); + } + }); + }); - done(); - } - }); + it('Should work with reactive queries via import', function (done) { + const query = postListExposure.clone({ + title: 'User Post - 3', }); - it('Should work with reactive queries containing link with foreignIdentityField', function (done) { - const query = productsList.clone({ - filters: { - // only considering products with productId - productId: {$ne: null}, - } - }); + const handle = query.subscribe(); - const handle = query.subscribe(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + const res = query.fetch(); + handle.stop(); - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const res = query.fetch(); - handle.stop(); + assert.isTrue(res.length > 0); - assert.equal(res.length, 3); + _.each(res, (post) => { + assert.equal(post.title, 'User Post - 3'); + assert.isObject(post.author); + assert.isObject(post.group); + }); - const [nails1, nails2, laptop] = res; - assert.equal(nails1.price, 1.50); - assert.lengthOf(nails1.attributes, 1); - assert.deepEqual(nails1.attributes[0].unit, 'piece'); - assert.deepEqual(nails1.attributes[0].delivery, 0); + done(); + } + }); + }); + + it('Should work with reactive queries containing link with foreignIdentityField', function (done) { + const query = productsList.clone({ + filters: { + // only considering products with productId + productId: { $ne: null }, + }, + // TODO(v3): sort not working + options: { + sort: { + ordering: 1, + }, + }, + }); - assert.equal(nails2.price, 1.60); - assert.lengthOf(nails2.attributes, 1); - assert.equal(nails2.attributes[0].unit, 'piece'); - assert.equal(nails2.attributes[0].delivery, 0); + /** + * @type {Meteor.SubscriptionHandle} + */ + const handle = query.subscribe(); + + Tracker.autorun((c) => { + if (handle.ready()) { + try { + c.stop(); + const res = query.fetch(); + handle.stop(); + + assert.equal(res.length, 3); + + const [nails1, nails2, laptop] = res; + + assert.equal(nails1.price, 1.5); + assert.lengthOf(nails1.attributes, 1); + assert.deepEqual(nails1.attributes[0].unit, 'piece'); + assert.deepEqual(nails1.attributes[0].delivery, 0); + + assert.equal(nails2.price, 1.6); + assert.lengthOf(nails2.attributes, 1); + assert.equal(nails2.attributes[0].unit, 'piece'); + assert.equal(nails2.attributes[0].delivery, 0); + + assert.equal(laptop.price, 1500); + assert.lengthOf(laptop.attributes, 1); + assert.equal(laptop.attributes[0].delivery, 10); + + done(); + } catch (e) { + done(e); + } + } + }); + }); - assert.equal(laptop.price, 1500); - assert.lengthOf(laptop.attributes, 1); - assert.equal(laptop.attributes[0].delivery, 10); + it('should work with reactive queries ', (done) => { + this.timeout(5000); - done(); - } - }); + const query = productsList.clone({ + filters: { + // only considering products with productId + singleProductId: { $ne: null }, + }, }); - it('should work with reactive queries ', (done) => { - const query = productsList.clone({ - filters: { - // only considering products with productId - singleProductId: {$ne: null}, - } - }); - - const handle = query.subscribe(); + const handle = query.subscribe(); - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const res = query.fetch(); - handle.stop(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + const res = query.fetch(); + handle.stop(); - assert.equal(res.length, 1); + assert.equal(res.length, 1); - const [laptop] = res; - assert.isObject(laptop.singleAttribute); - assert.equal(laptop.singleAttribute.delivery, 12); + const [laptop] = res; + assert.isObject(laptop.singleAttribute); + assert.equal(laptop.singleAttribute.delivery, 12); - done(); - } - }); + done(); + } }); + }); }); diff --git a/lib/namedQuery/testing/server.test.js b/lib/namedQuery/testing/server.test.js index 117bd9dd..4141606a 100755 --- a/lib/namedQuery/testing/server.test.js +++ b/lib/namedQuery/testing/server.test.js @@ -1,212 +1,211 @@ import { assert } from 'chai'; import { - postList, - postListCached, - postListResolver, - postListResolverCached, - postListParamsCheck, - postListParamsCheckServer, - productsList, - productAttributesList, + postList, + postListCached, + postListResolver, + postListResolverCached, + postListParamsCheck, + postListParamsCheckServer, + productsList, + productAttributesList, } from './bootstrap/queries'; import { createQuery, NamedQuery } from 'meteor/cultofcoders:grapher'; +import { _ } from 'meteor/underscore'; describe('Named Query', function () { - it('Should return the proper values', function () { - const createdQuery = createQuery({ - postList: { - title: 'User Post - 3' - } - }); - - const directQuery = postList.clone({ - title: 'User Post - 3' - }); - - _.each([createdQuery, directQuery], (query) => { - const data = query.fetch(); - - assert.isTrue(data.length > 1); - - _.each(data, post => { - assert.equal(post.title, 'User Post - 3'); - assert.isObject(post.author); - assert.isObject(post.group); - }) - }) + it('Should return the proper values', async function () { + const createdQuery = createQuery({ + postList: { + title: 'User Post - 3', + }, }); - it('Exposure embodyment should work properly', function () { - const query = createQuery({ - postListExposure: { - title: 'User Post - 3' - } - }); - - const data = query.fetch(); - - assert.isTrue(data.length > 1); - - _.each(data, post => { - assert.equal(post.title, 'User Post - 3'); - assert.isObject(post.author); - assert.isObject(post.group); - }) + const directQuery = postList.clone({ + title: 'User Post - 3', }); - it('Should properly cache the values', function (done) { - const posts = postListCached.fetch(); - const postsCount = postListCached.getCount(); - - const Posts = Mongo.Collection.get('posts'); - const postId = Posts.insert({title: 'Hello Cacher!'}); - - assert.equal(posts.length, postListCached.fetch().length); - assert.equal(postsCount, postListCached.getCount()); - - Meteor.setTimeout(function () { - const newPosts = postListCached.fetch(); - const newCount = postListCached.getCount(); - - Posts.remove(postId); + for await (const query of [createdQuery, directQuery]) { + const data = await query.fetchAsync(); - assert.isArray(newPosts); - assert.isNumber(newCount); + assert.isTrue(data.length > 1); - assert.equal(posts.length + 1, newPosts.length); - assert.equal(postsCount + 1, newCount); + for (const post of data) { + assert.equal(post.title, 'User Post - 3'); + assert.isObject(post.author); + assert.isObject(post.group); + } + } + }); - done(); - }, 400) + it('Exposure embodyment should work properly', async function () { + const query = createQuery({ + postListExposure: { + title: 'User Post - 3', + }, }); - it('Should allow to securely fetch a subbody of a namedQuery including embodiment', function () { - const query = createQuery({ - postListExposure: { - limit: 5, - title: 'User Post - 3', - $body: { - title: 1, - createdAt: 1, // should fail - group: { - name: 1, - createdAt: 1, // should fail - } - } - } - }); - - const data = query.fetch(); + const data = await query.fetchAsync(); - assert.isTrue(data.length > 0); + assert.isTrue(data.length > 1); - _.each(data, post => { - assert.equal(post.title, 'User Post - 3'); - assert.isUndefined(post.createdAt); - assert.isUndefined(post.author); - assert.isObject(post.group); - assert.isUndefined(post.group.createdAt); - }) + _.each(data, (post) => { + assert.equal(post.title, 'User Post - 3'); + assert.isObject(post.author); + assert.isObject(post.group); }); - - it('Should work with resolver() queries with params', function () { - const title = 'User Post - 3'; - const createdQuery = createQuery({ - postListResolver: { - title - } - }); - - const directQuery = postListResolver.clone({ - title - }); - - let data = createdQuery.fetch(); - assert.isArray(data); - assert.equal(title, data[0]); - - - data = directQuery.fetch(); - assert.isArray(data); - assert.equal(title, data[0]); + }); + + it('Should properly cache the values', async function (done) { + const posts = await postListCached.fetchAsync(); + const postsCount = await postListCached.getCountAsync(); + + const Posts = Mongo.Collection.get('posts'); + const postId = await Posts.insertAsync({ title: 'Hello Cacher!' }); + + assert.equal(posts.length, (await postListCached.fetchAsync()).length); + assert.equal(postsCount, await postListCached.getCountAsync()); + + Meteor.setTimeout(async function () { + const newPosts = await postListCached.fetchAsync(); + const newCount = await postListCached.getCountAsync(); + + await Posts.removeAsync(postId); + + assert.isArray(newPosts); + assert.isNumber(newCount); + + assert.equal(posts.length + 1, newPosts.length); + assert.equal(postsCount + 1, newCount); + + done(); + }, 400); + }); + + it('Should allow to securely fetch a subbody of a namedQuery including embodiment', async function () { + const query = createQuery({ + postListExposure: { + limit: 5, + title: 'User Post - 3', + $body: { + title: 1, + createdAt: 1, // should fail + group: { + name: 1, + createdAt: 1, // should fail + }, + }, + }, }); - it('Should work with resolver() that is cached', function () { - const title = 'User Post - 3'; - let data = postListResolverCached.clone({title}).fetch(); + const data = await query.fetchAsync(); - assert.isArray(data); - assert.equal(title, data[0]); + assert.isTrue(data.length > 0); - data = postListResolverCached.clone({title}).fetch(); - - assert.isArray(data); - assert.equal(title, data[0]); + _.each(data, (post) => { + assert.equal(post.title, 'User Post - 3'); + assert.isUndefined(post.createdAt); + assert.isUndefined(post.author); + assert.isObject(post.group); + assert.isUndefined(post.group.createdAt); }); - - it('Should work with resolver() that has params validation', function (done) { - try { - postListParamsCheck.clone({}).fetch(); - } catch (e) { - assert.isObject(e); - done(); - } + }); + + it('Should work with resolver() queries with params', async function () { + const title = 'User Post - 3'; + const createdQuery = createQuery({ + postListResolver: { + title, + }, }); - it('Should work with resolver() that has params server-side validation', function (done) { - try { - postListParamsCheckServer.clone({}).fetch(); - } catch (e) { - assert.isObject(e); - done(); - } + const directQuery = postListResolver.clone({ + title, }); - it('Should respect config set by NamedQuery.setConfig', () => { - NamedQuery.setConfig({scoped: true}); - try { - const query = createQuery('_namedQuery', { - posts: { - title: 1, - }, - }); - - assert.isTrue(query.options.scoped); - } - finally { - NamedQuery.setConfig({}); - } - }); - - it('Should work with foreign field - regular link assembly', () => { - const res = productAttributesList.clone().fetch(); - assert.lengthOf(res, 3); - - res.forEach(attribute => { - if (typeof attribute.productId === 'number') { - assert.isArray(attribute.products); - assert.isTrue(attribute.products.length > 0); - attribute.products.forEach(product => { - assert.equal(product.productId, attribute.productId); - }); - } - else { - assert.isUndefined(attribute.products); - } + let data = await createdQuery.fetchAsync(); + assert.isArray(data); + assert.equal(title, data[0]); + + data = await directQuery.fetchAsync(); + assert.isArray(data); + assert.equal(title, data[0]); + }); + + it('Should work with resolver() that is cached', async function () { + const title = 'User Post - 3'; + let data = await postListResolverCached.clone({ title }).fetchAsync(); + + assert.isArray(data); + assert.equal(title, data[0]); + + data = await postListResolverCached.clone({ title }).fetchAsync(); + + assert.isArray(data); + assert.equal(title, data[0]); + }); + + it('Should work with resolver() that has params validation', async function (done) { + try { + await postListParamsCheck.clone({}).fetchAsync(); + } catch (e) { + assert.isObject(e); + done(); + } + }); + + it('Should work with resolver() that has params server-side validation', async function (done) { + try { + await postListParamsCheckServer.clone({}).fetchAsync(); + console.log('done???'); + } catch (e) { + assert.isObject(e); + done(); + } + }); + + it('Should respect config set by NamedQuery.setConfig', () => { + NamedQuery.setConfig({ scoped: true }); + try { + const query = createQuery('_namedQuery', { + posts: { + title: 1, + }, + }); + + assert.isTrue(query.options.scoped); + } finally { + NamedQuery.setConfig({}); + } + }); + + it('Should work with foreign field - regular link assembly', async () => { + const res = await productAttributesList.clone().fetchAsync(); + assert.lengthOf(res, 3); + + res.forEach((attribute) => { + if (typeof attribute.productId === 'number') { + assert.isArray(attribute.products); + assert.isTrue(attribute.products.length > 0); + attribute.products.forEach((product) => { + assert.equal(product.productId, attribute.productId); }); + } else { + assert.isUndefined(attribute.products); + } }); + }); - it('Should work with foreign field - inversed link assembly', () => { - const res = productsList.clone({}).fetch(); + it('Should work with foreign field - inversed link assembly', async () => { + const res = await productsList.clone({}).fetchAsync(); - assert.lengthOf(res, 4); + assert.lengthOf(res, 4); - res.forEach(product => { - if ([1, 2].includes(product.productId)) { - assert.isArray(product.attributes); - assert.lengthOf(product.attributes, 1); - assert.equal(product.attributes[0].productId, product.productId); - } - }); + res.forEach((product) => { + if ([1, 2].includes(product.productId)) { + assert.isArray(product.attributes); + assert.lengthOf(product.attributes, 1); + assert.equal(product.attributes[0].productId, product.productId); + } }); + }); }); diff --git a/lib/query/counts/countSubscription.js b/lib/query/counts/countSubscription.js index 6fb543b1..4d948c13 100644 --- a/lib/query/counts/countSubscription.js +++ b/lib/query/counts/countSubscription.js @@ -5,105 +5,116 @@ import { Tracker } from 'meteor/tracker'; import Counts from './collection'; import createFauxSubscription from './createFauxSubscription'; -import prepareForProcess from '../lib/prepareForProcess.js'; -import NamedQueryBase from '../../namedQuery/namedQuery.base'; export default class CountSubscription { + /** + * @param {*} query - The query to use when fetching counts + */ + constructor(query) { /** - * @param {*} query - The query to use when fetching counts + * @type {ReactiveVar} */ - constructor(query) { - this.accessToken = new ReactiveVar(null); - this.fauxHandle = null; - this.query = query; + this.accessToken = new ReactiveVar(null); + this.fauxHandle = null; + this.query = query; + } + + /** + * Starts a subscription request for reactive counts. + * + * @param {*} arg - The argument to pass to {name}.count.subscribe + * @param {*} callback + */ + subscribe(arg, callback) { + // Don't try to resubscribe if arg hasn't changed + if (EJSON.equals(this.lastArgs, arg) && this.fauxHandle) { + return this.fauxHandle; } - /** - * Starts a subscription request for reactive counts. - * - * @param {*} arg - The argument to pass to {name}.count.subscribe - * @param {*} callback - */ - subscribe(arg, callback) { - // Don't try to resubscribe if arg hasn't changed - if (EJSON.equals(this.lastArgs, arg) && this.fauxHandle) { - return this.fauxHandle; - } + this.accessToken.set(null); + this.lastArgs = arg; - this.accessToken.set(null); - this.lastArgs = arg; + Meteor.callAsync(this.query.name + '.count.subscribe', arg) + .then((token) => { + if (!this._markedForUnsubscribe) { + this.subscriptionHandle = Meteor.subscribe( + this.query.name + '.count', + token, + callback, + ); + this.accessToken.set(token); - Meteor.call(this.query.name + '.count.subscribe', arg, (error, token) => { - if (!this._markedForUnsubscribe) { - this.subscriptionHandle = Meteor.subscribe(this.query.name + '.count', token, callback); - this.accessToken.set(token); + this.disconnectComputation = Tracker.autorun(() => + this.handleDisconnect(), + ); + } - this.disconnectComputation = Tracker.autorun(() => this.handleDisconnect()); - } + this._markedForUnsubscribe = false; + }) + .catch((err) => { + console.error('Count subscription failed', err); + }); - this._markedForUnsubscribe = false; - }); + this.fauxHandle = createFauxSubscription(this); + return this.fauxHandle; + } - this.fauxHandle = createFauxSubscription(this); - return this.fauxHandle; + /** + * Unsubscribes from the count endpoint, if there is such a subscription. + */ + unsubscribe() { + if (this.subscriptionHandle) { + this.disconnectComputation.stop(); + this.subscriptionHandle.stop(); + } else { + // If we hit this branch, then Meteor.call in subscribe hasn't finished yet + // so set a flag to stop the subscription from being created + this._markedForUnsubscribe = true; } - /** - * Unsubscribes from the count endpoint, if there is such a subscription. - */ - unsubscribe() { - if (this.subscriptionHandle) { - this.disconnectComputation.stop(); - this.subscriptionHandle.stop(); - } else { - // If we hit this branch, then Meteor.call in subscribe hasn't finished yet - // so set a flag to stop the subscription from being created - this._markedForUnsubscribe = true; - } + this.accessToken.set(null); + this.fauxHandle = null; + this.subscriptionHandle = null; + } - this.accessToken.set(null); - this.fauxHandle = null; - this.subscriptionHandle = null; - } + /** + * Reactively fetch current document count. Returns null if the subscription is not ready yet. + * + * @returns {Number|null} - Current document count + */ + getCount() { + const id = this.accessToken.get(); + if (id === null) return null; - /** - * Reactively fetch current document count. Returns null if the subscription is not ready yet. - * - * @returns {Number|null} - Current document count - */ - getCount() { - const id = this.accessToken.get(); - if (id === null) return null; + const doc = Counts.findOne(id); + return doc.count; + } - const doc = Counts.findOne(id); - return doc.count; + /** + * All session info gets deleted when the server goes down, so when the client attempts to + * optimistically resume the '.count' publication, the server will throw a 'no-request' error. + * + * This function prevents that by manually stopping and restarting the subscription when the + * connection to the server is lost. + */ + handleDisconnect() { + const status = Meteor.status(); + if (!status.connected) { + this._markedForResume = true; + this.fauxHandle = null; + this.subscriptionHandle.stop(); } - /** - * All session info gets deleted when the server goes down, so when the client attempts to - * optimistically resume the '.count' publication, the server will throw a 'no-request' error. - * - * This function prevents that by manually stopping and restarting the subscription when the - * connection to the server is lost. - */ - handleDisconnect() { - const status = Meteor.status(); - if (!status.connected) { - this._markedForResume = true; - this.fauxHandle = null; - this.subscriptionHandle.stop(); - } - - if (status.connected && this._markedForResume) { - this._markedForResume = false; - this.subscribe(this.lastArgs); - } + if (status.connected && this._markedForResume) { + this._markedForResume = false; + this.subscribe(this.lastArgs); } + } - /** - * Returns whether or not a subscription request has been made. - */ - isSubscribed() { - return this.accessToken.get() !== null; - } + /** + * Returns whether or not a subscription request has been made. + */ + isSubscribed() { + return this.accessToken.get() !== null; + } } diff --git a/lib/query/counts/createFauxSubscription.js b/lib/query/counts/createFauxSubscription.js index f09c98f3..a9fd5337 100644 --- a/lib/query/counts/createFauxSubscription.js +++ b/lib/query/counts/createFauxSubscription.js @@ -5,6 +5,8 @@ * @param {CountSubscription} countManager */ export default (countManager) => ({ - ready: () => countManager.accessToken.get() !== null && countManager.subscriptionHandle.ready(), - stop: () => countManager.unsubscribe(), + ready: () => + countManager.accessToken.get() !== null && + countManager.subscriptionHandle.ready(), + stop: () => countManager.unsubscribe(), }); diff --git a/lib/query/counts/genEndpoint.server.js b/lib/query/counts/genEndpoint.server.js index 29770e97..441e6ab6 100644 --- a/lib/query/counts/genEndpoint.server.js +++ b/lib/query/counts/genEndpoint.server.js @@ -1,4 +1,4 @@ -import {EJSON} from 'meteor/ejson'; +import { EJSON } from 'meteor/ejson'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; @@ -12,76 +12,79 @@ const collection = new Mongo.Collection(null); * This method generates a reactive count endpoint (a method and publication) for a collection or named query. * * @param {String} name - Name of the query or collection - * @param {Function} getCursor - Takes in the user's session document as an argument, and turns that into a Mongo cursor. - * @param {Function} getSession - Takes the subscribe method's argument as its parameter. Should enforce any necessary security constraints. The return value of this function is stored in the session document. + * @param {Object} options + * @param {Grapher.CountEndpointFunction} options.getCursor - Takes in the user's session document as an argument, and turns that into a Mongo cursor. + * @param {Function} options.getSession - Takes the subscribe method's argument as its parameter. Should enforce any necessary security constraints. The return value of this function is stored in the session document. */ export default (name, { getCursor, getSession }) => { - Meteor.methods({ - [name + '.count.subscribe'](paramsOrBody) { - const session = getSession.call(this, paramsOrBody); - const sessionId = EJSON.stringify(session); - - const existingSession = collection.findOne({ - session: sessionId, - userId: this.userId, - }); - - // Try to reuse sessions if the user subscribes multiple times with the same data - if (existingSession) { - return existingSession._id; - } - - const token = collection.insert({ - session: sessionId, - query: name, - userId: this.userId, - }); - - return token; - }, + Meteor.methods({ + async [name + '.count.subscribe'](paramsOrBody) { + const session = await getSession.call(this, paramsOrBody); + const sessionId = EJSON.stringify(session); + + const existingSession = await collection.findOneAsync({ + session: sessionId, + userId: this.userId, + }); + + // Try to reuse sessions if the user subscribes multiple times with the same data + if (existingSession) { + return existingSession._id; + } + + const token = await collection.insertAsync({ + session: sessionId, + query: name, + userId: this.userId, + }); + + return token; + }, + }); + + Meteor.publish(name + '.count', async function (token) { + check(token, String); + const self = this; + const request = await collection.findOneAsync({ + _id: token, + userId: self.userId, }); - Meteor.publish(name + '.count', function(token) { - check(token, String); - const self = this; - const request = collection.findOne({ _id: token, userId: self.userId }); - - if (!request) { - throw new Error( - 'no-request', - `You must acquire a request token via the "${name}.count.subscribe" method first.` - ); - } - - request.session = EJSON.parse(request.session); - const cursor = getCursor.call(this, request); - - // Start counting - let count = 0; - - let isReady = false; - const handle = cursor.observe({ - added() { - count++; - isReady && - self.changed(COUNTS_COLLECTION_CLIENT, token, { count }); - }, - - removed() { - count--; - isReady && - self.changed(COUNTS_COLLECTION_CLIENT, token, { count }); - }, - }); - - isReady = true; - self.added(COUNTS_COLLECTION_CLIENT, token, { count }); - - self.onStop(() => { - handle.stop(); - collection.remove(token); - }); - - self.ready(); + if (!request) { + throw new Error( + 'no-request', + `You must acquire a request token via the "${name}.count.subscribe" method first.`, + ); + } + + request.session = EJSON.parse(request.session); + const cursor = getCursor.call(this, request); + + // Start counting + let count = 0; + + let isReady = false; + // TODO(v3): new async function + const handle = await cursor.observeAsync({ + added() { + count++; + isReady && self.changed(COUNTS_COLLECTION_CLIENT, token, { count }); + }, + + removed() { + count--; + isReady && self.changed(COUNTS_COLLECTION_CLIENT, token, { count }); + }, }); + + isReady = true; + self.added(COUNTS_COLLECTION_CLIENT, token, { count }); + + self.onStop(async () => { + handle.stop(); + await collection.removeAsync(token); + }); + + self.ready(); + }); }; diff --git a/lib/query/counts/testing/client.test.js b/lib/query/counts/testing/client.test.js index bd2f1375..4b33390b 100755 --- a/lib/query/counts/testing/client.test.js +++ b/lib/query/counts/testing/client.test.js @@ -2,108 +2,109 @@ import { assert } from 'chai'; import { Tracker } from 'meteor/tracker'; import PostsCollection from './bootstrap/collection.test'; import NamedQuery, { - postsQuery, - postsQuery2, - postsQuery3, + postsQuery, + postsQuery2, + postsQuery3, } from './bootstrap/namedQuery.test'; -import callWithPromise from '../../lib/callWithPromise'; -describe('Reactive count tests', function() { - callWithPromise('resetPosts'); +describe('Reactive count tests', function () { + before(async () => { + await Meteor.callAsync('resetPosts'); + }); - it('Should fetch the initial count', function(done) { - const query = NamedQuery.clone(); - const handle = query.subscribeCount(); + it('Should fetch the initial count', function (done) { + const query = NamedQuery.clone(); + const handle = query.subscribeCount(); - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const count = query.getCount(); - handle.stop(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + const count = query.getCount(); + handle.stop(); - assert.equal(count, 3); - done(); - } - }); + assert.equal(count, 3); + done(); + } }); + }); - // TODO: Can these tests fail if assert gets called too quickly? - it('Should update when a document is added', function(done) { - const query = NamedQuery.clone(); - const handle = query.subscribeCount(); - - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const count = query.getCount(); - assert.equal(count, 3); - - Meteor.call('addPost', 'text4', (error, newId) => { - const newCount = query.getCount(); - assert.equal(newCount, 4); - - Meteor.call('removePost', newId); - handle.stop(); - done(); - }); - } - }); + // TODO: Can these tests fail if assert gets called too quickly? + it('Should update when a document is added', function (done) { + const query = NamedQuery.clone(); + const handle = query.subscribeCount(); + + Tracker.autorun(async (c) => { + if (handle.ready()) { + c.stop(); + const count = query.getCount(); + assert.equal(count, 3); + + const newId = await Meteor.callAsync('addPost', 'text4'); + const newCount = query.getCount(); + assert.equal(newCount, 4); + + await Meteor.callAsync('removePost', newId); + handle.stop(); + + done(); + } }); + }); + + it('Should update when a document is removed', function (done) { + const query = NamedQuery.clone(); + const handle = query.subscribeCount(); - it('Should update when a document is removed', function(done) { - const query = NamedQuery.clone(); - const handle = query.subscribeCount(); - - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); - const count = query.getCount(); - assert.equal(count, 3); - - Meteor.call('removePost', 'removeid', error => { - const newCount = query.getCount(); - assert.equal(newCount, 2); - - handle.stop(); - done(); - }); - } - }); + Tracker.autorun(async (c) => { + if (handle.ready()) { + c.stop(); + const count = query.getCount(); + assert.equal(count, 3); + + await Meteor.callAsync('removePost', 'removeid'); + + const newCount = query.getCount(); + assert.equal(newCount, 2); + + handle.stop(); + done(); + } }); + }); - it('Should work with two different queries', function(done) { - const query1 = postsQuery.clone(); - const query2 = postsQuery2.clone(); + it('Should work with two different queries', function (done) { + const query1 = postsQuery.clone(); + const query2 = postsQuery2.clone(); - const handle2 = query2.subscribeCount(); - const handle1 = query1.subscribeCount(); + const handle2 = query2.subscribeCount(); + const handle1 = query1.subscribeCount(); - Tracker.autorun(c => { - if (handle1.ready() && handle2.ready()) { - const count1 = query1.getCount(); - const count2 = query2.getCount(); + Tracker.autorun((c) => { + if (handle1.ready() && handle2.ready()) { + const count1 = query1.getCount(); + const count2 = query2.getCount(); - assert.equal(count1, 2); - assert.equal(count2, 1); - done(); - } - }); + assert.equal(count1, 2); + assert.equal(count2, 1); + done(); + } }); + }); - it('Should work with special filter params', function(done) { - const query = postsQuery3.clone({ - $regex: 'BOMB', - }); + it('Should work with special filter params', function (done) { + const query = postsQuery3.clone({ + $regex: 'BOMB', + }); - const handle = query.subscribeCount(); + const handle = query.subscribeCount(); - Tracker.autorun(c => { - if (handle.ready()) { - const count = query.getCount(); + Tracker.autorun((c) => { + if (handle.ready()) { + const count = query.getCount(); - assert.equal(count, 2); - done(); - } - }); + assert.equal(count, 2); + done(); + } }); + }); }); diff --git a/lib/query/counts/testing/server.test.js b/lib/query/counts/testing/server.test.js index 7667eb1c..d033388b 100644 --- a/lib/query/counts/testing/server.test.js +++ b/lib/query/counts/testing/server.test.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import PostsCollection from './bootstrap/collection.test'; import { - postsQuery, - postsQuery2, - postsQuery3, + postsQuery, + postsQuery2, + postsQuery3, } from './bootstrap/namedQuery.test'; postsQuery.expose(); @@ -11,18 +11,18 @@ postsQuery2.expose(); postsQuery3.expose(); Meteor.methods({ - resetPosts() { - PostsCollection.remove({}); - PostsCollection.insert({ text: 'text 1' }); - PostsCollection.insert({ text: 'text 2' }); - PostsCollection.insert({ _id: 'removeid', text: 'text 3' }); - }, + async resetPosts() { + await PostsCollection.removeAsync({}); + await PostsCollection.insertAsync({ text: 'text 1' }); + await PostsCollection.insertAsync({ text: 'text 2' }); + await PostsCollection.insertAsync({ _id: 'removeid', text: 'text 3' }); + }, - addPost(text) { - return PostsCollection.insert({ text }); - }, + async addPost(text) { + return PostsCollection.insertAsync({ text }); + }, - removePost(id) { - PostsCollection.remove({ _id: id }); - }, + async removePost(id) { + await PostsCollection.removeAsync({ _id: id }); + }, }); diff --git a/lib/query/hypernova/assembler.js b/lib/query/hypernova/assembler.js index 7da27f05..d42cb617 100644 --- a/lib/query/hypernova/assembler.js +++ b/lib/query/hypernova/assembler.js @@ -2,25 +2,26 @@ import createSearchFilters from '../../links/lib/createSearchFilters'; import cleanObjectForMetaFilters from './lib/cleanObjectForMetaFilters'; import sift from 'sift'; import dot from 'dot-object'; +import { _ } from 'meteor/underscore'; /** - * + * * getIdsForMany - * + * * Possible options: - * + * * A. array of ids directly inside the parentResult * { * projectIds: [...], * } - * + * * B. array of ids in nested document * { * nested: { * projectIds: [...], * } * } - * + * * Case for link with foreignIdentityField on projectId. This is still 'many' link because mapping could be one to many. * { * nested: { @@ -36,7 +37,7 @@ import dot from 'dot-object'; * projectIds: [...], * }] * } - * + * * Case with foreign identity field. * { * nestedArray: [{ @@ -47,244 +48,235 @@ import dot from 'dot-object'; * } */ function getIdsForMany(parentResult, fieldStorage) { - // support dotted fields - const [root, ...nested] = fieldStorage.split('.'); - const value = dot.pick(root, parentResult); + // support dotted fields + const [root, ...nested] = fieldStorage.split('.'); + const value = dot.pick(root, parentResult); - if (_.isUndefined(value) || _.isNull(value)) { - return []; - } + if (_.isUndefined(value) || _.isNull(value)) { + return []; + } - // Option A. - if (nested.length === 0) { - return Array.isArray(value) ? value : [value]; - } + // Option A. + if (nested.length === 0) { + return Array.isArray(value) ? value : [value]; + } - // Option C. - if (Array.isArray(value)) { - return _.flatten(value.map(v => getIdsFromObject(v, nested.join('.')))); - } - - // Option B - if (_.isObject(value)) { - return getIdsFromObject(value, nested.join('.')); - } + // Option C. + if (Array.isArray(value)) { + return _.flatten(value.map((v) => getIdsFromObject(v, nested.join('.')))); + } - return []; + // Option B + if (_.isObject(value)) { + return getIdsFromObject(value, nested.join('.')); + } + + return []; } function getIdsFromObject(object, path) { - const pickedValue = dot.pick(path, object); - return Array.isArray(pickedValue) ? pickedValue : ((_.isUndefined(pickedValue) || _.isNull(pickedValue)) ? [] : [pickedValue]); + const pickedValue = dot.pick(path, object); + return Array.isArray(pickedValue) + ? pickedValue + : _.isUndefined(pickedValue) || _.isNull(pickedValue) + ? [] + : [pickedValue]; } -export function assembleMany(parentResult, { - childCollectionNode, - linker, - limit, - skip, - resultsByKeyId, -}) { - const fieldStorage = linker.linkStorageField; - - const [root, ...rest] = fieldStorage.split('.'); - const rootValue = parentResult[root]; - if (!rootValue) { - return; - } - - const [, ...nestedLinkPath] = childCollectionNode.linkName.split('.'); - if (nestedLinkPath.length > 0 && Array.isArray(rootValue)) { - rootValue.forEach(result => { - const results = _.flatten(_.union(...getIdsForMany(result, rest.join('.')).map(id => resultsByKeyId[id]))); - const data = filterAssembledData( - results, - { limit, skip } - ); - result[nestedLinkPath.join('.')] = data; - }); - return; - } - - const results = _.union(...getIdsForMany(parentResult, fieldStorage).map(id => resultsByKeyId[id])); - const data = filterAssembledData( - results, - { limit, skip } - ); - dot.str(childCollectionNode.linkName, data, parentResult); +export function assembleMany( + parentResult, + { childCollectionNode, linker, limit, skip, resultsByKeyId }, +) { + const fieldStorage = linker.linkStorageField; + + const [root, ...rest] = fieldStorage.split('.'); + const rootValue = parentResult[root]; + if (!rootValue) { + return; + } + + const [, ...nestedLinkPath] = childCollectionNode.linkName.split('.'); + if (nestedLinkPath.length > 0 && Array.isArray(rootValue)) { + rootValue.forEach((result) => { + const results = _.flatten( + _.union( + ...getIdsForMany(result, rest.join('.')).map( + (id) => resultsByKeyId[id], + ), + ), + ); + const data = filterAssembledData(results, { limit, skip }); + result[nestedLinkPath.join('.')] = data; + }); + return; + } + + const results = _.union( + ...getIdsForMany(parentResult, fieldStorage).map( + (id) => resultsByKeyId[id], + ), + ); + const data = filterAssembledData(results, { limit, skip }); + dot.str(childCollectionNode.linkName, data, parentResult); } -export function assembleManyMeta(parentResult, { - childCollectionNode, - linker, - skip, - limit, - resultsByKeyId, -}) { - const fieldStorage = linker.linkStorageField; - - const _ids = _.pluck(parentResult[fieldStorage], '_id'); - let data = []; - _ids.forEach(_id => { - data.push(_.first(resultsByKeyId[_id])); - }); +export function assembleManyMeta( + parentResult, + { childCollectionNode, linker, skip, limit, resultsByKeyId }, +) { + const fieldStorage = linker.linkStorageField; - parentResult[childCollectionNode.linkName] = filterAssembledData( - data, - { limit, skip } - ); -} + const _ids = _.pluck(parentResult[fieldStorage], '_id'); + let data = []; + _ids.forEach((_id) => { + data.push(_.first(resultsByKeyId[_id])); + }); -export function assembleOneMeta(parentResult, { - childCollectionNode, - linker, + parentResult[childCollectionNode.linkName] = filterAssembledData(data, { limit, skip, - resultsByKeyId, -}) { - const fieldStorage = linker.linkStorageField; - - if (!parentResult[fieldStorage]) { - return; - } - - const _id = parentResult[fieldStorage]._id; - parentResult[childCollectionNode.linkName] = filterAssembledData( - resultsByKeyId[_id], - { limit, skip } - ); + }); } -export function assembleOne(parentResult, { - childCollectionNode, - linker, - limit, - skip, - resultsByKeyId, -}) { - const fieldStorage = linker.linkStorageField; - - const [root, ...rest] = fieldStorage.split('.'); - const rootValue = parentResult[root]; - if (!rootValue) { - return; - } +export function assembleOneMeta( + parentResult, + { childCollectionNode, linker, limit, skip, resultsByKeyId }, +) { + const fieldStorage = linker.linkStorageField; + + if (!parentResult[fieldStorage]) { + return; + } + + const _id = parentResult[fieldStorage]._id; + parentResult[childCollectionNode.linkName] = filterAssembledData( + resultsByKeyId[_id], + { limit, skip }, + ); +} - // todo: using linker.linkName should be correct here since it should be the same as childCollectionNode.linkName - const path = childCollectionNode.linkName.split('.'); - - if (Array.isArray(rootValue)) { - rootValue.forEach(result => { - const value = dot.pick(rest.join('.'), result); - const data = filterAssembledData( - resultsByKeyId[value], - { limit, skip } - ); - dot.set(path.slice(1).join('.'), data, result); - // result[path.slice(1).join('.')] = data; - }); - return; - } +export function assembleOne( + parentResult, + { childCollectionNode, linker, limit, skip, resultsByKeyId }, +) { + const fieldStorage = linker.linkStorageField; + + const [root, ...rest] = fieldStorage.split('.'); + const rootValue = parentResult[root]; + if (!rootValue) { + return; + } + + // todo: using linker.linkName should be correct here since it should be the same as childCollectionNode.linkName + const path = childCollectionNode.linkName.split('.'); + + if (Array.isArray(rootValue)) { + rootValue.forEach((result) => { + const value = dot.pick(rest.join('.'), result); + const data = filterAssembledData(resultsByKeyId[value], { limit, skip }); + dot.set(path.slice(1).join('.'), data, result); + // result[path.slice(1).join('.')] = data; + }); + return; + } - const value = dot.pick(fieldStorage, parentResult); - if (!value) { - return; - } + const value = dot.pick(fieldStorage, parentResult); + if (!value) { + return; + } - const data = filterAssembledData( - resultsByKeyId[value], - { limit, skip } - ); - dot.str(childCollectionNode.linkName, data, parentResult); + const data = filterAssembledData(resultsByKeyId[value], { limit, skip }); + dot.str(childCollectionNode.linkName, data, parentResult); } export default (childCollectionNode, { limit, skip, metaFilters }) => { - if (childCollectionNode.results.length === 0) { - return; - } - - const parent = childCollectionNode.parent; - const linker = childCollectionNode.linker; - - const strategy = linker.strategy; - const isMeta = linker.isMeta(); - const fieldStorage = linker.linkStorageField; - - // cleaning the parent results from a child - // this may be the wrong approach but it works for now - if (isMeta && metaFilters) { - const metaFiltersTest = sift(metaFilters); - _.each(parent.results, parentResult => { - cleanObjectForMetaFilters( - parentResult, - fieldStorage, - metaFiltersTest - ); - }); - } - - const resultsByKeyId = _.groupBy(childCollectionNode.results, linker.foreignIdentityField); - - if (childCollectionNode.linkName !== linker.linkName) { - throw new Error(`error: ${childCollectionNode.linkName} ${linker.linkName}`); - } - - if (strategy === 'one') { - parent.results.forEach(parentResult => { - return assembleOne(parentResult, { - childCollectionNode, - linker, - limit, - skip, - resultsByKeyId, - }); - }); - } - - if (strategy === 'many') { - parent.results.forEach(parentResult => { - return assembleMany(parentResult, { - childCollectionNode, - linker, - skip, - limit, - resultsByKeyId, - }); - }); - } + if (childCollectionNode.results.length === 0) { + return; + } + + const parent = childCollectionNode.parent; + const linker = childCollectionNode.linker; + + const strategy = linker.strategy; + const isMeta = linker.isMeta(); + const fieldStorage = linker.linkStorageField; + + // cleaning the parent results from a child + // this may be the wrong approach but it works for now + if (isMeta && metaFilters) { + const metaFiltersTest = sift(metaFilters); + _.each(parent.results, (parentResult) => { + cleanObjectForMetaFilters(parentResult, fieldStorage, metaFiltersTest); + }); + } - if (strategy === 'one-meta') { - parent.results.forEach(parentResult => { - return assembleOneMeta(parentResult, { - linker, - childCollectionNode, - limit, - skip, - resultsByKeyId, - }) - }); - } + const resultsByKeyId = _.groupBy( + childCollectionNode.results, + linker.foreignIdentityField, + ); - if (strategy === 'many-meta') { - parent.results.forEach(parentResult => { - return assembleManyMeta(parentResult, { - childCollectionNode, - linker, - limit, - skip, - resultsByKeyId, - }); - }); - } + if (childCollectionNode.linkName !== linker.linkName) { + throw new Error( + `error: ${childCollectionNode.linkName} ${linker.linkName}`, + ); + } + + if (strategy === 'one') { + parent.results.forEach((parentResult) => { + return assembleOne(parentResult, { + childCollectionNode, + linker, + limit, + skip, + resultsByKeyId, + }); + }); + } + + if (strategy === 'many') { + parent.results.forEach((parentResult) => { + return assembleMany(parentResult, { + childCollectionNode, + linker, + skip, + limit, + resultsByKeyId, + }); + }); + } + + if (strategy === 'one-meta') { + parent.results.forEach((parentResult) => { + return assembleOneMeta(parentResult, { + linker, + childCollectionNode, + limit, + skip, + resultsByKeyId, + }); + }); + } + + if (strategy === 'many-meta') { + parent.results.forEach((parentResult) => { + return assembleManyMeta(parentResult, { + childCollectionNode, + linker, + limit, + skip, + resultsByKeyId, + }); + }); + } }; function filterAssembledData(data, { limit, skip }) { - if (Array.isArray(data)) { - data = data.filter(Boolean); - if (limit) { - return data.slice(skip, limit); - } + if (Array.isArray(data)) { + data = data.filter(Boolean); + if (limit) { + return data.slice(skip, limit); } + } - return data; + return data; } diff --git a/lib/query/hypernova/hypernova.js b/lib/query/hypernova/hypernova.js index 6ee840ed..c07b765a 100755 --- a/lib/query/hypernova/hypernova.js +++ b/lib/query/hypernova/hypernova.js @@ -2,27 +2,47 @@ import applyProps from '../lib/applyProps.js'; import prepareForDelivery from '../lib/prepareForDelivery.js'; import storeHypernovaResults from './storeHypernovaResults.js'; -function hypernova(collectionNode, userId) { - _.each(collectionNode.collectionNodes, childCollectionNode => { - storeHypernovaResults(childCollectionNode, userId); - hypernova(childCollectionNode, userId); - }); +/** + * + * @param {*} collectionNode + * @param {string} [userId] + */ +async function hypernova(collectionNode, userId) { + for await (const childCollectionNode of collectionNode.collectionNodes) { + await storeHypernovaResults(childCollectionNode, userId); + await hypernova(childCollectionNode, userId); + } } -export default function hypernovaInit(collectionNode, userId, config = {}) { - const bypassFirewalls = config.bypassFirewalls || false; - const params = config.params || {}; - - let {filters, options} = applyProps(collectionNode); - - const collection = collectionNode.collection; - - collectionNode.results = collection.find(filters, options, userId).fetch(); - - const userIdToPass = (config.bypassFirewalls) ? undefined : userId; - hypernova(collectionNode, userIdToPass); - - prepareForDelivery(collectionNode, params); - - return collectionNode.results; +/** + * + * @template P={Grapher.Params} + * + * @param {*} collectionNode + * @param {string} [userId] + * @param {Grapher.HypernovaConfig

} [config] + * @returns + */ +export default async function hypernovaInit( + collectionNode, + userId, + config = {}, +) { + const bypassFirewalls = config.bypassFirewalls || false; + const params = config.params || {}; + + let { filters, options } = applyProps(collectionNode); + + const collection = collectionNode.collection; + + collectionNode.results = await collection + .find(filters, options, userId) + .fetchAsync(); + + const userIdToPass = config.bypassFirewalls ? undefined : userId; + await hypernova(collectionNode, userIdToPass); + + prepareForDelivery(collectionNode, params); + + return collectionNode.results; } diff --git a/lib/query/hypernova/storeHypernovaResults.js b/lib/query/hypernova/storeHypernovaResults.js index a21cc8de..90b7298f 100644 --- a/lib/query/hypernova/storeHypernovaResults.js +++ b/lib/query/hypernova/storeHypernovaResults.js @@ -3,63 +3,109 @@ import AggregateFilters from './aggregateSearchFilters.js'; import assemble from './assembler.js'; import processVirtualNode from './processVirtualNode.js'; import buildVirtualNodeProps from './buildVirtualNodeProps.js'; +import { _ } from 'meteor/underscore'; -export default function storeHypernovaResults(childCollectionNode, userId) { - if (childCollectionNode.parent.results.length === 0) { - return (childCollectionNode.results = []); - } +/** + * + * @param {*} childCollectionNode + * @param {string} [userId] + * @returns {Promise} + */ +export default async function storeHypernovaResults( + childCollectionNode, + userId, +) { + if (childCollectionNode.parent.results.length === 0) { + return (childCollectionNode.results = []); + } - let { filters, options } = applyProps(childCollectionNode); + let { filters, options } = applyProps(childCollectionNode); - const metaFilters = filters.$meta; - const aggregateFilters = new AggregateFilters( - childCollectionNode, - metaFilters + const metaFilters = filters.$meta; + const aggregateFilters = new AggregateFilters( + childCollectionNode, + metaFilters, + ); + delete filters.$meta; + + const linker = childCollectionNode.linker; + const isVirtual = linker.isVirtual(); + const collection = childCollectionNode.collection; + + _.extend(filters, aggregateFilters.create()); + + // if it's not virtual then we retrieve them and assemble them here. + if (!isVirtual) { + const filteredOptions = _.omit(options, 'limit'); + + childCollectionNode.results = await collection + .find(filters, filteredOptions, userId) + .fetchAsync(); + + assemble(childCollectionNode, { + ...options, + metaFilters, + }); + } else { + // virtuals arrive here + const virtualProps = buildVirtualNodeProps( + childCollectionNode, + filters, + options, + userId, ); - delete filters.$meta; + const { + filters: virtualFilters, + options: { limit, skip, ...virtualOptions }, + } = virtualProps; + // console.log(JSON.stringify(virtualProps, null, 4)); - const linker = childCollectionNode.linker; - const isVirtual = linker.isVirtual(); - const collection = childCollectionNode.collection; + const results = await collection + .find(virtualFilters, virtualOptions) + .fetchAsync(); - _.extend(filters, aggregateFilters.create()); + // console.log(JSON.stringify(results, null, 4)); // if it's not virtual then we retrieve them and assemble them here. if (!isVirtual) { - const filteredOptions = _.omit(options, 'limit'); + const filteredOptions = _.omit(options, 'limit'); - childCollectionNode.results = collection - .find(filters, filteredOptions, userId) - .fetch(); + childCollectionNode.results = await collection + .find(filters, filteredOptions, userId) + .fetchAsync(); - assemble(childCollectionNode, { - ...options, - metaFilters, - }); + assemble(childCollectionNode, { + ...options, + metaFilters, + }); } else { - // virtuals arrive here - const virtualProps = buildVirtualNodeProps( - childCollectionNode, - filters, - options, - userId - ); - - const {filters: virtualFilters, options: {limit, skip, ...virtualOptions}} = virtualProps; - - // console.log(JSON.stringify(virtualProps, null, 4)); - - const results = collection.find(virtualFilters, virtualOptions).fetch(); - - // console.log(JSON.stringify(results, null, 4)); - - processVirtualNode( - childCollectionNode, - results, - metaFilters, - {limit, skip}, - ); + // virtuals arrive here + const virtualProps = buildVirtualNodeProps( + childCollectionNode, + filters, + options, + userId, + ); + + const { + filters: virtualFilters, + options: { limit, skip, ...virtualOptions }, + } = virtualProps; + + // console.log(JSON.stringify(virtualProps, null, 4)); + + const results = await collection + .find(virtualFilters, virtualOptions) + .fetchAsync(); + + // console.log(JSON.stringify(results, null, 4)); + + processVirtualNode(childCollectionNode, results, metaFilters, { + limit, + skip, + }); } + } } diff --git a/lib/query/lib/applyProps.js b/lib/query/lib/applyProps.js index b4934c9b..082033be 100755 --- a/lib/query/lib/applyProps.js +++ b/lib/query/lib/applyProps.js @@ -1,17 +1,19 @@ +import { _ } from 'meteor/underscore'; + const restrictOptions = [ - 'disableOplog', - 'pollingIntervalMs', - 'pollingThrottleMs' + 'disableOplog', + 'pollingIntervalMs', + 'pollingThrottleMs', ]; export default function applyProps(node) { - let filters = Object.assign({}, node.props.$filters); - let options = Object.assign({}, node.props.$options); + let filters = Object.assign({}, node.props.$filters); + let options = Object.assign({}, node.props.$options); + + options = _.omit(options, ...restrictOptions); + options.fields = options.fields || {}; - options = _.omit(options, ...restrictOptions); - options.fields = options.fields || {}; + node.applyFields(filters, options); - node.applyFields(filters, options); - - return {filters, options}; + return { filters, options }; } diff --git a/lib/query/lib/callWithPromise.js b/lib/query/lib/callWithPromise.js deleted file mode 100644 index 536e9e2e..00000000 --- a/lib/query/lib/callWithPromise.js +++ /dev/null @@ -1,9 +0,0 @@ -export default (method, myParameters) => { - return new Promise((resolve, reject) => { - Meteor.call(method, myParameters, (err, res) => { - if (err) reject(err.reason || 'Something went wrong.'); - - resolve(res); - }); - }); -}; \ No newline at end of file diff --git a/lib/query/lib/createGraph.js b/lib/query/lib/createGraph.js index 2611c238..725324ae 100755 --- a/lib/query/lib/createGraph.js +++ b/lib/query/lib/createGraph.js @@ -4,13 +4,14 @@ import FieldNode from '../nodes/fieldNode.js'; import ReducerNode from '../nodes/reducerNode.js'; import dotize from './dotize.js'; import createReducers from '../reducers/lib/createReducers'; +import { _ } from 'meteor/underscore'; export const specialFields = [ - '$filters', - '$options', - '$postFilters', - '$postOptions', - '$postFilter' + '$filters', + '$options', + '$postFilters', + '$postOptions', + '$postFilter', ]; /** @@ -19,148 +20,156 @@ export const specialFields = [ * @param root */ export function createNodes(root) { - // this is a fix for phantomjs tests (don't really understand it.) - if (!_.isObject(root.body)) { - return; + // this is a fix for phantomjs tests (don't really understand it.) + if (!_.isObject(root.body)) { + return; + } + + _.each(root.body, (body, fieldName) => { + if (!body) { + return; } - _.each(root.body, (body, fieldName) => { - if (!body) { - return; - } - - // if it's a prop - if (_.contains(specialFields, fieldName)) { - root.addProp(fieldName, body); + // if it's a prop + if (_.contains(specialFields, fieldName)) { + root.addProp(fieldName, body); - return; - } + return; + } - // workaround, see https://github.com/cult-of-coders/grapher/issues/134 - // TODO: find another way to do this - if (root.collection.default) { - root.collection = root.collection.default; - } + // workaround, see https://github.com/cult-of-coders/grapher/issues/134 + // TODO: find another way to do this + if (root.collection.default) { + root.collection = root.collection.default; + } - // checking if it is a link. - let linker = root.collection.getLinker(fieldName); - - if (linker) { - // check if it is a cached link - // if yes, then we need to explicitly define this at collection level - // so when we transform the data for delivery, we move it to the link name - if (linker.isDenormalized()) { - if (linker.isSubBodyDenormalized(body)) { - handleDenormalized(root, linker, body, fieldName); - return; - } - } - - let subroot = new CollectionNode(linker.getLinkedCollection(), body, fieldName); - // must be before adding linker because _shouldCleanStorage method - createNodes(subroot); - root.add(subroot, linker); - - return; + // checking if it is a link. + let linker = root.collection.getLinker(fieldName); + + if (linker) { + // check if it is a cached link + // if yes, then we need to explicitly define this at collection level + // so when we transform the data for delivery, we move it to the link name + if (linker.isDenormalized()) { + if (linker.isSubBodyDenormalized(body)) { + handleDenormalized(root, linker, body, fieldName); + return; } + } + + let subroot = new CollectionNode( + linker.getLinkedCollection(), + body, + fieldName, + ); + // must be before adding linker because _shouldCleanStorage method + createNodes(subroot); + root.add(subroot, linker); + + return; + } - // checking if it's a reducer - const reducer = root.collection.getReducer(fieldName); + // checking if it's a reducer + const reducer = root.collection.getReducer(fieldName); - if (reducer) { - let reducerNode = new ReducerNode(fieldName, reducer); - root.add(reducerNode); - } + if (reducer) { + let reducerNode = new ReducerNode(fieldName, reducer); + root.add(reducerNode); + } - // it's most likely a field then - addFieldNode(body, fieldName, root); - }); + // it's most likely a field then + addFieldNode(body, fieldName, root); + }); - createReducers(root); + createReducers(root); - if (root.fieldNodes.length === 0) { - root.add(new FieldNode('_id', 1)); - } + if (root.fieldNodes.length === 0) { + root.add(new FieldNode('_id', 1)); + } } function isProjectionOperatorExpression(body) { - if (_.isObject(body)) { - const keys = _.keys(body); - return keys.length === 1 && _.contains(['$elemMatch', '$meta', '$slice'], keys[0]); - } - return false; + if (_.isObject(body)) { + const keys = _.keys(body); + return ( + keys.length === 1 && + _.contains(['$elemMatch', '$meta', '$slice'], keys[0]) + ); + } + return false; } function tryFindLink(root, dottizedPath) { - // This would be the link in form of {nestedDocument: {linkedCollection: {...fields...}}} - const parts = dottizedPath.split('.'); - const firstPart = parts.slice(0, 2); - // Here we have a situation where we have link inside a nested document of a nested document - // {nestedDocument: {subnestedDocument: {linkedCollection: {...fields...}}} - const nestedParts = parts.slice(2); - - const potentialLinks = nestedParts.reduce((acc, part) => { - return [ - ...acc, - `${_.last(acc)}.${part}`, - ]; - }, [firstPart.join('.')]); - - // Trying to find topmost link - while (potentialLinks[0]) { - const linkerKey = potentialLinks.splice(0, 1); - const linker = root.collection.getLinker(linkerKey); - if (linker) { - return linker; - } + // This would be the link in form of {nestedDocument: {linkedCollection: {...fields...}}} + const parts = dottizedPath.split('.'); + const firstPart = parts.slice(0, 2); + // Here we have a situation where we have link inside a nested document of a nested document + // {nestedDocument: {subnestedDocument: {linkedCollection: {...fields...}}} + const nestedParts = parts.slice(2); + + const potentialLinks = nestedParts.reduce( + (acc, part) => { + return [...acc, `${_.last(acc)}.${part}`]; + }, + [firstPart.join('.')], + ); + + // Trying to find topmost link + while (potentialLinks[0]) { + const linkerKey = potentialLinks.splice(0, 1); + const linker = root.collection.getLinker(linkerKey); + if (linker) { + return linker; } + } } - /** * @param body * @param fieldName * @param root */ export function addFieldNode(body, fieldName, root) { - // it's not a link and not a special variable => we assume it's a field - if (_.isObject(body)) { - if (!isProjectionOperatorExpression(body)) { - let dotted = dotize.convert({[fieldName]: body}); - _.each(dotted, (value, key) => { - // check for link - const linker = tryFindLink(root, key); - - if (linker && !root.hasCollectionNode(linker.linkName)) { - const path = linker.linkName.split('.').slice(1).join('.'); - const subrootBody = dot.pick(path, body); - - const subroot = new CollectionNode(linker.getLinkedCollection(), subrootBody, linker.linkName); - // must be before adding linker because _shouldCleanStorage method - createNodes(subroot); - root.add(subroot, linker); - return; - } - - - // checking if it's a reducer - const reducer = root.collection.getReducer(key); - - if (reducer) { - let reducerNode = new ReducerNode(key, reducer); - root.add(reducerNode); - } else { - root.add(new FieldNode(key, value)); - } - }); + // it's not a link and not a special variable => we assume it's a field + if (_.isObject(body)) { + if (!isProjectionOperatorExpression(body)) { + let dotted = dotize.convert({ [fieldName]: body }); + _.each(dotted, (value, key) => { + // check for link + const linker = tryFindLink(root, key); + + if (linker && !root.hasCollectionNode(linker.linkName)) { + const path = linker.linkName.split('.').slice(1).join('.'); + const subrootBody = dot.pick(path, body); + + const subroot = new CollectionNode( + linker.getLinkedCollection(), + subrootBody, + linker.linkName, + ); + // must be before adding linker because _shouldCleanStorage method + createNodes(subroot); + root.add(subroot, linker); + return; } - else { - root.add(new FieldNode(fieldName, body, true)); + + // checking if it's a reducer + const reducer = root.collection.getReducer(key); + + if (reducer) { + let reducerNode = new ReducerNode(key, reducer); + root.add(reducerNode); + } else { + root.add(new FieldNode(key, value)); } + }); } else { - let fieldNode = new FieldNode(fieldName, body); - root.add(fieldNode); + root.add(new FieldNode(fieldName, body, true)); } + } else { + let fieldNode = new FieldNode(fieldName, body); + root.add(fieldNode); + } } /** @@ -170,16 +179,18 @@ export function addFieldNode(body, fieldName, root) { * @returns {String} */ export function getNodeNamespace(node) { - const parts = []; - let n = node; - while (n) { - // links can now contain '.' (nested links) - const name = n.linker ? n.linker.linkName.replace(/\./, '_') : n.collection._name; - parts.push(name); - // console.log('linker', node.linker ? node.linker.linkName : node.collection._name); - n = n.parent; - } - return parts.reverse().join('_'); + const parts = []; + let n = node; + while (n) { + // links can now contain '.' (nested links) + const name = n.linker + ? n.linker.linkName.replace(/\./, '_') + : n.collection._name; + parts.push(name); + // console.log('linker', node.linker ? node.linker.linkName : node.collection._name); + n = n.parent; + } + return parts.reverse().join('_'); } /** @@ -188,11 +199,11 @@ export function getNodeNamespace(node) { * @returns {CollectionNode} */ export default function (collection, body) { - let root = new CollectionNode(collection, body); - createNodes(root); + let root = new CollectionNode(collection, body); + createNodes(root); - return root; -}; + return root; +} /** * Ads denormalization config properly, including _id @@ -203,15 +214,15 @@ export default function (collection, body) { * @param fieldName */ function handleDenormalized(root, linker, body, fieldName) { - Object.assign(body, {_id: 1}); + Object.assign(body, { _id: 1 }); - const cacheField = linker.linkConfig.denormalize.field; - root.snapCache(cacheField, fieldName); + const cacheField = linker.linkConfig.denormalize.field; + root.snapCache(cacheField, fieldName); - // if it's one and direct also include the link storage - if (!linker.isMany() && !linker.isVirtual()) { - addFieldNode(1, linker.linkStorageField, root); - } + // if it's one and direct also include the link storage + if (!linker.isMany() && !linker.isVirtual()) { + addFieldNode(1, linker.linkStorageField, root); + } - addFieldNode(body, cacheField, root); -} \ No newline at end of file + addFieldNode(body, cacheField, root); +} diff --git a/lib/query/lib/prepareForProcess.js b/lib/query/lib/prepareForProcess.js index 8e3b80eb..1d20b054 100644 --- a/lib/query/lib/prepareForProcess.js +++ b/lib/query/lib/prepareForProcess.js @@ -1,85 +1,81 @@ -import {check, Match} from 'meteor/check'; +import { check, Match } from 'meteor/check'; import deepClone from 'lodash.clonedeep'; -function defaultFilterFunction({ - filters, - options, - params -}) { - if (params.filters) { - Object.assign(filters, params.filters); - } - if (params.options) { - Object.assign(options, params.options); - } +function defaultFilterFunction({ filters, options, params }) { + if (params.filters) { + Object.assign(filters, params.filters); + } + if (params.options) { + Object.assign(options, params.options); + } } function applyFilterRecursive(data, params = {}, isRoot = false) { - if (isRoot && !_.isFunction(data.$filter)) { - data.$filter = defaultFilterFunction; - } + if (isRoot && !_.isFunction(data.$filter)) { + data.$filter = defaultFilterFunction; + } - if (data.$filter) { - check(data.$filter, Match.OneOf(Function, [Function])); + if (data.$filter) { + check(data.$filter, Match.OneOf(Function, [Function])); - data.$filters = data.$filters || {}; - data.$options = data.$options || {}; + data.$filters = data.$filters || {}; + data.$options = data.$options || {}; - if (Array.isArray(data.$filter)) { - data.$filter.forEach(filter => { - filter.call(null, { - filters: data.$filters, - options: data.$options, - params: params - }) - }); - } else { - data.$filter({ - filters: data.$filters, - options: data.$options, - params: params - }); - } - - data.$filter = null; - delete(data.$filter); + if (Array.isArray(data.$filter)) { + data.$filter.forEach((filter) => { + filter.call(null, { + filters: data.$filters, + options: data.$options, + params: params, + }); + }); + } else { + data.$filter({ + filters: data.$filters, + options: data.$options, + params: params, + }); } - _.each(data, (value, key) => { - if (_.isObject(value)) { - return applyFilterRecursive(value, params); - } - }) + data.$filter = null; + delete data.$filter; + } + + _.each(data, (value, key) => { + if (_.isObject(value)) { + return applyFilterRecursive(value, params); + } + }); } function applyPagination(body, _params) { - if (body['$paginate'] && _params) { - if (!body.$options) { - body.$options = {}; - } - - if (_params.limit) { - _.extend(body.$options, { - limit: _params.limit - }) - } + if (body['$paginate'] && _params) { + if (!body.$options) { + body.$options = {}; + } - if (_params.skip) { - _.extend(body.$options, { - skip: _params.skip - }) - } + if (_params.limit) { + _.extend(body.$options, { + limit: _params.limit, + }); + } - delete body['$paginate']; + if (_params.skip) { + _.extend(body.$options, { + skip: _params.skip, + }); } + + delete body['$paginate']; + } } export default (_body, _params = {}) => { - let body = deepClone(_body); - let params = deepClone(_params); + let body = deepClone(_body); + let params = deepClone(_params); - applyPagination(body, params); - applyFilterRecursive(body, params, true); + applyPagination(body, params); + applyFilterRecursive(body, params, true); - return body; -} + return body; +}; diff --git a/lib/query/lib/recursiveCompose.js b/lib/query/lib/recursiveCompose.js index 58d66aa2..4edd2aec 100755 --- a/lib/query/lib/recursiveCompose.js +++ b/lib/query/lib/recursiveCompose.js @@ -1,84 +1,91 @@ +import { _ } from 'meteor/underscore'; import applyProps from './applyProps.js'; -import {getNodeNamespace} from './createGraph'; -import {isFieldInProjection} from './fieldInProjection'; +import { getNodeNamespace } from './createGraph'; +import { isFieldInProjection } from './fieldInProjection'; /** * Adds _query_path fields to the cursor docs which are used for scoped query filtering on the client. - * - * @param cursor - * @param ns + * + * @param {Mongo.Cursor} cursor + * @param {string} ns */ function patchCursor(cursor, ns) { - const originalObserve = cursor.observe; - cursor.observe = function (callbacks) { - const newCallbacks = Object.assign({}, callbacks); - if (callbacks.added) { - newCallbacks.added = doc => { - doc = _.clone(doc); - doc[`_query_path_${ns}`] = 1; - callbacks.added(doc); - }; - } - return originalObserve.call(cursor, newCallbacks); - }; + const originalObserve = cursor.observe; + cursor.observe = function (callbacks) { + const newCallbacks = Object.assign({}, callbacks); + if (callbacks.added) { + newCallbacks.added = (doc) => { + doc = _.clone(doc); + doc[`_query_path_${ns}`] = 1; + callbacks.added(doc); + }; + } + return originalObserve.call(cursor, newCallbacks); + }; } function compose(node, userId, config) { - return { - find(parent) { - if (parent) { - if (!config.blocking) { - this.unblock(); - } + return { + find(parent) { + if (parent) { + if (!config.blocking) { + this.unblock(); + } - let {filters, options} = applyProps(node); + let { filters, options } = applyProps(node); - // composition - let linker = node.linker; - let accessor = linker.createLink(parent); + // composition + let linker = node.linker; + let accessor = linker.createLink(parent); - // the rule is this, if a child I want to fetch is virtual, then I want to fetch the link storage of those fields - if (linker.isVirtual()) { - options.fields = options.fields || {}; - if (!isFieldInProjection(options.fields, linker.linkStorageField, true)) { - _.extend(options.fields, { - [linker.linkStorageField]: 1 - }); - } - } + // the rule is this, if a child I want to fetch is virtual, then I want to fetch the link storage of those fields + if (linker.isVirtual()) { + options.fields = options.fields || {}; + if ( + !isFieldInProjection(options.fields, linker.linkStorageField, true) + ) { + _.extend(options.fields, { + [linker.linkStorageField]: 1, + }); + } + } - const cursor = accessor.find(filters, options, userId); - if (config.scoped) { - patchCursor(cursor, getNodeNamespace(node)); - } - return cursor; - } - }, + const cursor = accessor.find(filters, options, userId); + if (config.scoped) { + patchCursor(cursor, getNodeNamespace(node)); + } + return cursor; + } + }, - children: _.map(node.collectionNodes, n => compose(n, userId, config)) - } + children: _.map(node.collectionNodes, (n) => compose(n, userId, config)), + }; } -export default (node, userId, config = {bypassFirewalls: false, scoped: false}) => { - return { - find() { - if (!config.blocking) { - this.unblock(); - } +export default ( + node, + userId, + config = { bypassFirewalls: false, scoped: false }, +) => { + return { + find() { + if (!config.blocking) { + this.unblock(); + } - let {filters, options} = applyProps(node); + let { filters, options } = applyProps(node); - const cursor = node.collection.find(filters, options, userId); - if (config.scoped) { - patchCursor(cursor, getNodeNamespace(node)); - } - return cursor; - }, + const cursor = node.collection.find(filters, options, userId); + if (config.scoped) { + patchCursor(cursor, getNodeNamespace(node)); + } + return cursor; + }, - children: _.map(node.collectionNodes, n => { - const userIdToPass = (config.bypassFirewalls) ? undefined : userId; + children: _.map(node.collectionNodes, (n) => { + const userIdToPass = config.bypassFirewalls ? undefined : userId; - return compose(n, userIdToPass, config); - }) - } -} + return compose(n, userIdToPass, config); + }), + }; +}; diff --git a/lib/query/query.base.js b/lib/query/query.base.js index 86907706..e65b92b4 100644 --- a/lib/query/query.base.js +++ b/lib/query/query.base.js @@ -1,58 +1,70 @@ import deepClone from 'lodash.clonedeep'; -import {check} from 'meteor/check'; +import { check } from 'meteor/check'; +import { _ } from 'meteor/underscore'; +/** + * @template T + * @template {Grapher.Params} P Params type + */ export default class QueryBase { - isGlobalQuery = true; + isGlobalQuery = true; - constructor(collection, body, options = {}) { - this.collection = collection; + /** + * + * @param {Mongo.Collection} collection + * @param {Grapher.Body} body + * @param {Grapher.QueryOptions} options + */ + constructor(collection, body, options = {}) { + this.collection = collection; - this.body = deepClone(body); + this.body = deepClone(body); - this.params = options.params || {}; - this.options = options; - } + this.params = options.params || {}; + this.options = options; + } - clone(newParams) { - const params = _.extend({}, deepClone(this.params), newParams); - - return new this.constructor( - this.collection, - deepClone(this.body), - { - params, - ...this.options - } - ); - } + /** + * + * @param {P} newParams + * @returns + */ + clone(newParams) { + const params = _.extend({}, deepClone(this.params), newParams); - get name() { - return `exposure_${this.collection._name}`; - } + return new this.constructor(this.collection, deepClone(this.body), { + params, + ...this.options, + }); + } - /** - * Validates the parameters - */ - doValidateParams() { - const {validateParams} = this.options; - if (!validateParams) return; - - if (_.isFunction(validateParams)) { - validateParams.call(null, this.params) - } else { - check(this.params) - } - } + get name() { + return `exposure_${this.collection._name}`; + } - /** - * Merges the params with previous params. - * - * @param params - * @returns {Query} - */ - setParams(params) { - this.params = _.extend({}, this.params, params); + /** + * Validates the parameters + */ + doValidateParams() { + const { validateParams } = this.options; + if (!validateParams) return; - return this; + if (_.isFunction(validateParams)) { + validateParams.call(null, this.params); + } else { + check(this.params); } -} \ No newline at end of file + } + + /** + * Merges the params with previous params. + * + * @param {P} params + * @returns {this} + */ + setParams(params) { + this.params = _.extend({}, this.params, params); + + return this; + } +} diff --git a/lib/query/query.client.js b/lib/query/query.client.js index 4b706a7a..a5068376 100644 --- a/lib/query/query.client.js +++ b/lib/query/query.client.js @@ -3,186 +3,217 @@ import CountSubscription from './counts/countSubscription'; import createGraph from './lib/createGraph.js'; import recursiveFetch from './lib/recursiveFetch.js'; import prepareForProcess from './lib/prepareForProcess.js'; -import callWithPromise from './lib/callWithPromise'; import Base from './query.base'; export default class Query extends Base { - /** - * Subscribe - * - * @param callback {Function} optional - * @returns {null|any|*} - */ - subscribe(callback) { - this.doValidateParams(); - - this.subscriptionHandle = Meteor.subscribe( - this.name, - prepareForProcess(this.body, this.params), - callback - ); - - return this.subscriptionHandle; + /** + * Subscribe + * + * @param callback {Function} optional + * @returns {null|any|*} + */ + subscribe(callback) { + this.doValidateParams(); + + this.subscriptionHandle = Meteor.subscribe( + this.name, + prepareForProcess(this.body, this.params), + callback, + ); + + return this.subscriptionHandle; + } + + /** + * Subscribe to the counts for this query + * + * @param callback + * @returns {Object} + */ + subscribeCount(callback) { + this.doValidateParams(); + + if (!this._counter) { + this._counter = new CountSubscription(this); } - /** - * Subscribe to the counts for this query - * - * @param callback - * @returns {Object} - */ - subscribeCount(callback) { - this.doValidateParams(); - - if (!this._counter) { - this._counter = new CountSubscription(this); - } - - return this._counter.subscribe( - prepareForProcess(this.body, this.params), - callback - ); + return this._counter.subscribe( + prepareForProcess(this.body, this.params), + callback, + ); + } + + /** + * Unsubscribe if an existing subscription exists + */ + unsubscribe() { + if (this.subscriptionHandle) { + this.subscriptionHandle.stop(); } - /** - * Unsubscribe if an existing subscription exists - */ - unsubscribe() { - if (this.subscriptionHandle) { - this.subscriptionHandle.stop(); - } - - this.subscriptionHandle = null; - } - - /** - * Unsubscribe to the counts if a subscription exists. - */ - unsubscribeCount() { - if (this._counter) { - this._counter.unsubscribe(); - this._counter = null; - } - } - - /** - * Fetches elements in sync using promises - * @return {*} - */ - async fetchSync() { - this.doValidateParams(); - - if (this.subscriptionHandle) { - throw new Meteor.Error('This query is reactive, meaning you cannot use promises to fetch the data.'); - } + this.subscriptionHandle = null; + } - return await callWithPromise(this.name, prepareForProcess(this.body, this.params)); + /** + * Unsubscribe to the counts if a subscription exists. + */ + unsubscribeCount() { + if (this._counter) { + this._counter.unsubscribe(); + this._counter = null; } - - /** - * Fetches one element in sync - * @return {*} - */ - async fetchOneSync() { - return _.first(await this.fetchSync()) + } + + /** + * @deprecated Use fetchAsync + */ + fetchSync() { + return this.fetchAsync(); + } + + /** + * Fetches elements in sync using promises + * @return {*} + */ + async fetchAsync() { + if (this.subscriptionHandle) { + throw new Meteor.Error( + 'This query is reactive, meaning you cannot use promises to fetch the data.', + ); } - /** - * Retrieves the data. - * @param callbackOrOptions - * @returns {*} - */ - fetch(callbackOrOptions) { - this.doValidateParams(); - - if (!this.subscriptionHandle) { - return this._fetchStatic(callbackOrOptions) - } else { - return this._fetchReactive(callbackOrOptions); - } + this.doValidateParams(); + return Meteor.callAsync( + this.name, + prepareForProcess(this.body, this.params), + ); + } + + /** + * @deprecated Use fetchOneAsync + */ + async fetchOneSync() { + return this.fetchOneAsync(); + } + + /** + * Fetches one element in sync + * @return {Promise} + */ + async fetchOneAsync() { + return _.first(await this.fetchAsync()); + } + + /** + * Retrieves the data. + * @param callbackOrOptions + * @returns {*} + */ + fetch(callbackOrOptions) { + this.doValidateParams(); + + if (!this.subscriptionHandle) { + this._fetchStatic().then( + (res) => { + callbackOrOptions(undefined, res); + }, + (err) => { + callbackOrOptions(err, undefined); + }, + ); + } else { + return this._fetchReactive(callbackOrOptions); } - - /** - * @param args - * @returns {*} - */ - fetchOne(...args) { - if (!this.subscriptionHandle) { - const callback = args[0]; - if (!_.isFunction(callback)) { - throw new Meteor.Error('You did not provide a valid callback'); - } - - this.fetch((err, res) => { - callback(err, res ? _.first(res) : null); - }) - } else { - return _.first(this.fetch(...args)); - } + } + + /** + * @param args + * @returns {*} + */ + fetchOne(...args) { + if (!this.subscriptionHandle) { + const callback = args[0]; + if (!_.isFunction(callback)) { + throw new Meteor.Error('You did not provide a valid callback'); + } + + this.fetch((err, res) => { + callback(err, res ? _.first(res) : null); + }); + } else { + return _.first(this.fetch(...args)); } - - /** - * Gets the count of matching elements in sync. - * @returns {any} - */ - async getCountSync() { - if (this._counter) { - throw new Meteor.Error('This query is reactive, meaning you cannot use promises to fetch the data.'); - } - - return await callWithPromise(this.name + '.count', prepareForProcess(this.body, this.params)); + } + + /** + * @deprecated Use getCountAsync + * + */ + async getCountSync() { + return this.getCountAsync(); + } + + /** + * Gets the count of matching elements in sync. + * @returns {Promise} + */ + async getCountAsync() { + if (this._counter) { + throw new Meteor.Error( + 'This query is reactive, meaning you cannot use promises to fetch the data.', + ); } - /** - * Gets the count of matching elements. - * @param callback - * @returns {any} - */ - getCount(callback) { - if (this._counter) { - return this._counter.getCount(); - } else { - if (!callback) { - throw new Meteor.Error('not-allowed', 'You are on client so you must either provide a callback to get the count or subscribe first.'); - } else { - return Meteor.call( - this.name + '.count', - prepareForProcess(this.body, this.params), - callback - ); - } - } + return Meteor.callAsync( + this.name + '.count', + prepareForProcess(this.body, this.params), + ); + } + + /** + * Gets the count of matching elements. + * @param callback + * @returns {any} + */ + getCount(callback) { + if (this._counter) { + return this._counter.getCount(); + } else { + if (!callback) { + throw new Meteor.Error( + 'not-allowed', + 'You are on client so you must either provide a callback to get the count or subscribe first.', + ); + } else { + return Meteor.call( + this.name + '.count', + prepareForProcess(this.body, this.params), + callback, + ); + } } - - /** - * Fetching non-reactive queries - * @param callback - * @private - */ - _fetchStatic(callback) { - if (!callback) { - throw new Meteor.Error('not-allowed', 'You are on client so you must either provide a callback to get the data or subscribe first.'); - } - - Meteor.call(this.name, prepareForProcess(this.body, this.params), callback); + } + + _fetchStatic() { + return Meteor.callAsync( + this.name, + prepareForProcess(this.body, this.params), + ); + } + + /** + * Fetching when we've got an active publication + * + * @param options + * @returns {*} + * @private + */ + _fetchReactive(options = {}) { + let body = prepareForProcess(this.body, this.params); + if (!options.allowSkip && body.$options && body.$options.skip) { + delete body.$options.skip; } - /** - * Fetching when we've got an active publication - * - * @param options - * @returns {*} - * @private - */ - _fetchReactive(options = {}) { - let body = prepareForProcess(this.body, this.params); - if (!options.allowSkip && body.$options && body.$options.skip) { - delete body.$options.skip; - } - - return recursiveFetch( - createGraph(this.collection, body), - this.params - ); - } + return recursiveFetch(createGraph(this.collection, body), this.params); + } } diff --git a/lib/query/query.js b/lib/query/query.js index d8d4fd92..5cce844d 100644 --- a/lib/query/query.js +++ b/lib/query/query.js @@ -1,12 +1,15 @@ import QueryClient from './query.client'; import QueryServer from './query.server'; +/** + * @type {typeof QueryClient | typeof QueryServer} + */ let Query; if (Meteor.isServer) { - Query = QueryServer; + Query = QueryServer; } else { - Query = QueryClient; + Query = QueryClient; } -export default Query; \ No newline at end of file +export default Query; diff --git a/lib/query/query.server.js b/lib/query/query.server.js index 215c416e..368a8281 100644 --- a/lib/query/query.server.js +++ b/lib/query/query.server.js @@ -2,37 +2,74 @@ import createGraph from './lib/createGraph.js'; import prepareForProcess from './lib/prepareForProcess.js'; import hypernova from './hypernova/hypernova.js'; import Base from './query.base'; +import { _ } from 'meteor/underscore'; +/** + * @template T + * @template P=Grapher.Params Params type + * @extends Base + */ export default class Query extends Base { - /** - * Retrieves the data. - * @param context - * @returns {*} - */ - fetch(context = {}) { - const node = createGraph( - this.collection, - prepareForProcess(this.body, this.params) - ); + /** + * Retrieves the data. + * @param {Grapher.QueryFetchContext} context + * @returns {*} + */ + fetch(context = {}) { + throw new Error( + 'You must call fetchAsync instead of fetch from server-side', + ); + } - return hypernova(node, context.userId, {params: this.params}); - } + /** + * + * @param {Grapher.QueryFetchContext} [context] + */ + fetchAsync(context = {}) { + const node = createGraph( + this.collection, + prepareForProcess(this.body, this.params), + ); - /** - * @param context - * @returns {*} - */ - fetchOne(context = {}) { - context.$options = context.$options || {}; - context.$options.limit = 1; - return _.first(this.fetch(context)); - } + return hypernova(node, context.userId, { params: this.params }); + } - /** - * Gets the count of matching elements. - * @returns {integer} - */ - getCount() { - return this.collection.find(this.body.$filters || {}, {}).count(); - } + /** + * @param {Grapher.QueryFetchContext} [context] + * @returns {*} + */ + fetchOne(context = {}) { + throw new Error( + 'You must call fetchOneAsync instead of fetchOne from server-side', + ); + } + + /** + * + * @param {Grapher.QueryFetchContext} [context] + * @returns {Promise<*>} + */ + async fetchOneAsync(context = {}) { + context.$options = context.$options || {}; + context.$options.limit = 1; + const results = await this.fetchAsync(context); + return _.first(results); + } + /** + * Gets the count of matching elements. + * @returns {number} + */ + getCount() { + throw new Error( + 'You must call countAsync instead of count from server-side', + ); + } + + /** + * Gets the count of matching elements. + * @returns {Promise} + */ + getCountAsync() { + return this.collection.find(this.body.$filters || {}, {}).countAsync(); + } } diff --git a/lib/query/testing/bootstrap/fixtures.js b/lib/query/testing/bootstrap/fixtures.js index 0f710b65..595dee0f 100755 --- a/lib/query/testing/bootstrap/fixtures.js +++ b/lib/query/testing/bootstrap/fixtures.js @@ -8,20 +8,20 @@ import Posts from './posts/collection'; import Tags from './tags/collection'; import Groups from './groups/collection'; import Users from './users/collection'; -import {Files} from './files/collection'; -import {Projects} from './projects/collection'; -import {Products, ProductAttributes} from './products/collection'; - -Authors.remove({}); -Comments.remove({}); -Posts.remove({}); -Tags.remove({}); -Groups.remove({}); -Users.remove({}); -Files.remove({}); -Projects.remove({}); -Products.remove({}); -ProductAttributes.remove({}); +import { Files } from './files/collection'; +import { Projects } from './projects/collection'; +import { Products, ProductAttributes } from './products/collection'; + +await Authors.removeAsync({}); +await Comments.removeAsync({}); +await Posts.removeAsync({}); +await Tags.removeAsync({}); +await Groups.removeAsync({}); +await Users.removeAsync({}); +await Files.removeAsync({}); +await Projects.removeAsync({}); +await Products.removeAsync({}); +await ProductAttributes.removeAsync({}); const AUTHORS = 6; const POST_PER_USER = 6; @@ -29,153 +29,168 @@ const COMMENTS_PER_POST = 6; const USERS = 4; const TAGS = ['JavaScript', 'Meteor', 'React', 'Other']; const GROUPS = ['JavaScript', 'Meteor', 'React', 'Other']; -const COMMENT_TEXT_SAMPLES = [ - 'Good', 'Bad', 'Neutral' -]; +const COMMENT_TEXT_SAMPLES = ['Good', 'Bad', 'Neutral']; console.log('[testing] Loading test fixtures ...'); -let tags = TAGS.map(name => Tags.insert({name})); -let groups = GROUPS.map(name => Groups.insert({ - name, - createdAt: new Date(), -})); -let authors = _.range(AUTHORS).map(idx => { - return Authors.insert({ - name: 'Author - ' + idx, - profile: { - firstName: 'First Name - ' + idx, - lastName: 'Last Name - ' + idx - } +let tags = await Promise.all(TAGS.map((name) => Tags.insertAsync({ name }))); +let groups = await Promise.all( + GROUPS.map((name) => + Groups.insertAsync({ + name, + createdAt: new Date(), + }), + ), +); +let authors = await Promise.all( + _.range(AUTHORS).map((idx) => { + return Authors.insertAsync({ + name: 'Author - ' + idx, + profile: { + firstName: 'First Name - ' + idx, + lastName: 'Last Name - ' + idx, + }, }); -}); + }), +); let idx = 1; -_.each(authors, (author) => { - idx++; - const authorPostLink = Authors.getLink(author, 'posts'); - const authorGroupLink = Authors.getLink(author, 'groups'); - - authorGroupLink.add(groups[idx % 4], { - isAdmin: _.sample([true, false]) - }); - - _.each(_.range(POST_PER_USER), (idx) => { - let post = { - title: `User Post - ${idx}`, - metadata: { - keywords: _.sample(TAGS, _.random(1, 2)), - language: { - ..._.sample([{abbr: 'en', title: 'English'}, {abbr: 'de', title: 'Deutsch'}]), - } - }, - createdAt: new Date(), - }; - - authorPostLink.add(post); - const postCommentsLink = Posts.getLink(post, 'comments'); - const postTagsLink = Posts.getLink(post, 'tags'); - const postGroupLink = Posts.getLink(post, 'group'); - postGroupLink.set(_.sample(groups), {random: Random.id()}); - - postTagsLink.add(_.sample(tags)); - - _.each(_.range(COMMENTS_PER_POST), (idx) => { - let comment = { - text: _.sample(COMMENT_TEXT_SAMPLES) - }; - - postCommentsLink.add(comment); - Comments.getLink(comment, 'author').set(_.sample(authors)); - }) - }) -}); +for (const author of authors) { + idx++; + const authorPostLink = await Authors.getLink(author, 'posts'); + const authorGroupLink = await Authors.getLink(author, 'groups'); + + await authorGroupLink.add(groups[idx % 4], { + isAdmin: _.sample([true, false]), + }); + + for (const idx of _.range(POST_PER_USER)) { + let post = { + title: `User Post - ${idx}`, + metadata: { + keywords: _.sample(TAGS, _.random(1, 2)), + language: { + ..._.sample([ + { abbr: 'en', title: 'English' }, + { abbr: 'de', title: 'Deutsch' }, + ]), + }, + }, + createdAt: new Date(), + }; + + await authorPostLink.add(post); + const postCommentsLink = await Posts.getLink(post, 'comments'); + const postTagsLink = await Posts.getLink(post, 'tags'); + const postGroupLink = await Posts.getLink(post, 'group'); + await postGroupLink.set(_.sample(groups), { random: Random.id() }); + + await postTagsLink.add(_.sample(tags)); + + for (const commentIdx of _.range(COMMENTS_PER_POST)) { + let comment = { + text: _.sample(COMMENT_TEXT_SAMPLES), + }; + + await postCommentsLink.add(comment); + await (await Comments.getLink(comment, 'author')).set(_.sample(authors)); + } + } +} const friendIds = []; // each user is created so his friends are previously added users -_.range(USERS).forEach(idx => { - const id = Users.insert({ - name: `User - ${idx}`, - friendIds, - subordinateIds: idx === 3 ? [friendIds[2]] : [], - }); - - friendIds.push(id); -}); - -const project1 = Projects.insert({name: 'Project 1'}); -const project2 = Projects.insert({name: 'Project 2'}); - -Files.insert({ - filename: 'test.txt', - metas: [{ - type: 'text', - projectId: project1, - projectIds: [project1], - }, { - type: 'hidden', - projectId: project2, - projectIds: [project2, project1], - }], - meta: { - type: 'text', - projectId: project1, - projectIds: [project2], +for (const idx of _.range(USERS)) { + const id = await Users.insertAsync({ + name: `User - ${idx}`, + friendIds, + subordinateIds: idx === 3 ? [friendIds[2]] : [], + }); + + friendIds.push(id); +} + +const project1 = await Projects.insertAsync({ name: 'Project 1' }); +const project2 = await Projects.insertAsync({ name: 'Project 2' }); + +await Files.insertAsync({ + filename: 'test.txt', + metas: [ + { + type: 'text', + projectId: project1, + projectIds: [project1], + }, + { + type: 'hidden', + projectId: project2, + projectIds: [project2, project1], }, + ], + meta: { + type: 'text', + projectId: project1, + projectIds: [project2], + }, }); -Files.insert({ - filename: 'invoice.pdf', - metas: [{ - type: 'pdf', - projectId: project2, - }], - meta: { - type: 'pdf', - projectId: project1, - projectIds: [project2, project1], +await Files.insertAsync({ + filename: 'invoice.pdf', + metas: [ + { + type: 'pdf', + projectId: project2, }, + ], + meta: { + type: 'pdf', + projectId: project1, + projectIds: [project2, project1], + }, }); /** * Products here are slowly changing dimensions, with price being the the field which creates new version. * However, there are also general attributes which do not create versions, such as units & delivery. */ -Products.insert({ - title: 'Nails', - price: 1.50, - productId: 1, +await Products.insertAsync({ + title: 'Nails', + price: 1.5, + productId: 1, + ordering: 1, }); -Products.insert({ - title: 'Nails', - price: 1.60, - productId: 1, +await Products.insertAsync({ + title: 'Nails', + price: 1.6, + productId: 1, + ordering: 2, }); -Products.insert({ - title: 'Laptop', - price: 1500, - productId: 2, +await Products.insertAsync({ + title: 'Laptop', + price: 1500, + productId: 2, + ordering: 3, }); -ProductAttributes.insert({ - productId: 1, - unit: 'piece', - delivery: 0, +await ProductAttributes.insertAsync({ + productId: 1, + unit: 'piece', + delivery: 0, }); -ProductAttributes.insert({ - productId: 2, - delivery: 10, +await ProductAttributes.insertAsync({ + productId: 2, + delivery: 10, }); // For testing "one" relationship on product -Products.insert({ - title: 'Laptop', - price: 1300, - singleProductId: 1, +await Products.insertAsync({ + title: 'Laptop', + price: 1300, + singleProductId: 1, }); -ProductAttributes.insert({ - singleProductId: 1, - delivery: 12, +await ProductAttributes.insertAsync({ + singleProductId: 1, + delivery: 12, }); console.log('[ok] fixtures have been loaded.'); diff --git a/lib/query/testing/bootstrap/posts/links.js b/lib/query/testing/bootstrap/posts/links.js index ac6d2aaf..b0784ae0 100755 --- a/lib/query/testing/bootstrap/posts/links.js +++ b/lib/query/testing/bootstrap/posts/links.js @@ -5,65 +5,65 @@ import Tags from '../tags/collection.js'; import Groups from '../groups/collection.js'; Posts.addLinks({ - author: { - type: 'one', - collection: Authors, - field: 'ownerId', - index: true - }, - comments: { - collection: Comments, - inversedBy: 'post' - }, - tags: { - collection: Tags, - type: 'many', - field: 'tagIds', - index: true - }, - group: { - type: 'one', - collection: Groups, - metadata: true, - field: 'groupId' + author: { + type: 'one', + collection: Authors, + field: 'ownerId', + index: true, + }, + comments: { + collection: Comments, + inversedBy: 'post', + }, + tags: { + collection: Tags, + type: 'many', + field: 'tagIds', + index: true, + }, + group: { + type: 'one', + collection: Groups, + metadata: true, + field: 'groupId', + }, + authorCached: { + type: 'one', + collection: Authors, + field: 'ownerId', + denormalize: { + field: 'authorCache', + body: { + name: 1, + profile: { + firstName: 1, + lastName: 1, + }, + }, }, - authorCached: { - type: 'one', - collection: Authors, - field: 'ownerId', - denormalize: { - field: 'authorCache', - body: { - name: 1, - profile: { - firstName: 1, - lastName: 1, - } - } - } + }, + tagsCached: { + collection: Tags, + type: 'many', + field: 'tagIds', + denormalize: { + field: 'tagsCache', + body: { + name: 1, + }, }, - tagsCached: { - collection: Tags, - type: 'many', - field: 'tagIds', - denormalize: { - field: 'tagsCache', - body: { - name: 1 - } - } - } + }, }); Posts.addReducers({ - reducerNonExistentNestedField: { - body: { - nested: { - title: 1, - } - }, - reduce(object) { - return object.nested ? object.nested.title : 'null'; - } + reducerNonExistentNestedField: { + body: { + nested: { + title: 1, + }, + }, + reduce(object) { + return object.nested ? object.nested.title : 'null'; }, -}); \ No newline at end of file + }, +}); diff --git a/lib/query/testing/bootstrap/security/collection.js b/lib/query/testing/bootstrap/security/collection.js index 360054ea..92eea455 100644 --- a/lib/query/testing/bootstrap/security/collection.js +++ b/lib/query/testing/bootstrap/security/collection.js @@ -6,15 +6,13 @@ const SubItems = new Mongo.Collection('security_subitems'); export { Items, SubItems }; if (Meteor.isServer) { - Items.expose({ - firewall(filters, options, userId) { + Items.expose({ + firewall(filters, options, userId) {}, + }); - } - }); - - SubItems.expose({ - firewall(filters, options, userId) { - filters._id = false; - } - }); -} \ No newline at end of file + SubItems.expose({ + firewall(filters, options, userId) { + filters._id = false; + }, + }); +} diff --git a/lib/query/testing/bootstrap/security/fixtures.js b/lib/query/testing/bootstrap/security/fixtures.js index 970c1074..1986307c 100644 --- a/lib/query/testing/bootstrap/security/fixtures.js +++ b/lib/query/testing/bootstrap/security/fixtures.js @@ -1,10 +1,12 @@ import { Items, SubItems } from './collection'; -Items.remove({}); -SubItems.remove({}); +await Items.removeAsync({}); +await SubItems.removeAsync({}); -const itemsId = Items.insert({text: 'hello'}); +const itemsId = await Items.insertAsync({ text: 'hello' }); -Items.getLink(itemsId, 'subitems').add({ - text: 'hello from subitem' -}); \ No newline at end of file +await ( + await Items.getLink(itemsId, 'subitems') +).add({ + text: 'hello from subitem', +}); diff --git a/lib/query/testing/bootstrap/users/links.js b/lib/query/testing/bootstrap/users/links.js index cd3c431e..2612c2cd 100755 --- a/lib/query/testing/bootstrap/users/links.js +++ b/lib/query/testing/bootstrap/users/links.js @@ -2,14 +2,14 @@ import Users from './collection'; Users.addLinks({ friends: { - collection: Users, - field: 'friendIds', - type: 'many' + collection: Users, + field: 'friendIds', + type: 'many', }, subordinates: { collection: Users, field: 'subordinateIds', - type: 'many' + type: 'many', }, friendsCached: { collection: Users, @@ -18,8 +18,8 @@ Users.addLinks({ denormalize: { field: 'friendsCache', body: { - name: 1 - } - } -}, + name: 1, + }, + }, + }, }); diff --git a/lib/query/testing/client.test.js b/lib/query/testing/client.test.js index 2fc4d1bb..e5457bc0 100755 --- a/lib/query/testing/client.test.js +++ b/lib/query/testing/client.test.js @@ -3,716 +3,718 @@ import { createQuery } from 'meteor/cultofcoders:grapher'; import './reducers.client.test'; import './security.client.test'; import waitForHandleToBeReady from './lib/waitForHandleToBeReady'; +import { _ } from 'meteor/underscore'; describe('Query Client Tests', function () { - it('Should work with queries via method call', function (done) { - const query = createQuery({ - posts: { - $options: {limit: 5}, - title: 1, - comments: { - $filters: {text: 'Good'}, - text: 1 - } - } - }); - - query.fetch((err, res) => { - assert.isUndefined(err); - - assert.isArray(res); - _.each(res, post => { - assert.isString(post.title); - assert.isString(post._id); - _.each(post.comments, comment => { - assert.isString(comment._id); - assert.equal('Good', comment.text); - }) - }); - - done(); - }); + it('Should work with queries via method call', function (done) { + const query = createQuery({ + posts: { + $options: { limit: 5 }, + title: 1, + comments: { + $filters: { text: 'Good' }, + text: 1, + }, + }, }); - it('Should work with queries reactively', function (done) { - const query = createQuery({ - posts: { - $options: {limit: 5}, - title: 1, - comments: { - $filters: {text: 'Good'}, - text: 1 - } - } - }); - - const handle = query.subscribe(); + query.fetch((err, res) => { + try { + assert.isUndefined(err); - Tracker.autorun(c => { - if (handle.ready()) { - c.stop(); + assert.isArray(res); + _.each(res, (post) => { + assert.isString(post.title); + assert.isString(post._id); + _.each(post.comments, (comment) => { + assert.isString(comment._id); + assert.equal('Good', comment.text); + }); + }); - const res = query.fetch(); + done(); + } catch (err) { + done(err); + } + }); + }); + + it('Should work with queries reactively', function (done) { + const query = createQuery({ + posts: { + $options: { limit: 5 }, + title: 1, + comments: { + $filters: { text: 'Good' }, + text: 1, + }, + }, + }); - assert.isArray(res); - _.each(res, post => { - assert.isString(post.title); - assert.isString(post._id); - _.each(post.comments, comment => { - assert.isString(comment._id); - assert.equal('Good', comment.text); - }) - }); + const handle = query.subscribe(); - handle.stop(); - done(); - } - }) - }); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + const res = query.fetch(); - it('Should fetch direct One links with $metadata via Subscription', function (done) { - let query = createQuery({ - posts: { - group: { - name: 1 - } - } + assert.isArray(res); + _.each(res, (post) => { + assert.isString(post.title); + assert.isString(post._id); + _.each(post.comments, (comment) => { + assert.isString(comment._id); + assert.equal('Good', comment.text); + }); }); + handle.stop(); - let handle = query.subscribe(); - Tracker.autorun((c) => { - if (handle.ready()) { - c.stop(); - let data = query.fetch(); - - handle.stop(); + done(); + } + }); + }); + + it('Should fetch direct One links with $metadata via Subscription', function (done) { + let query = createQuery({ + posts: { + group: { + name: 1, + }, + }, + }); - _.each(data, post => { - assert.isObject(post.group.$metadata); - assert.isDefined(post.group.$metadata.random); - }); + let handle = query.subscribe(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + let data = query.fetch(); - done(); - } - }) - }); + handle.stop(); - it('Should fetch direct Many links with $metadata via Subscription', function (done) { - let query = createQuery({ - authors: { - groups: { - $options: {limit: 1}, - name: 1 - } - } + _.each(data, (post) => { + assert.isObject(post.group.$metadata); + assert.isDefined(post.group.$metadata.random); }); - let handle = query.subscribe(); - Tracker.autorun((c) => { - if (handle.ready()) { - c.stop(); - let data = query.fetch(); - - handle.stop(); + done(); + } + }); + }); + + it('Should fetch direct Many links with $metadata via Subscription', function (done) { + let query = createQuery({ + authors: { + groups: { + $options: { limit: 1 }, + name: 1, + }, + }, + }); - _.each(data, author => { - assert.isArray(author.groups); + let handle = query.subscribe(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + let data = query.fetch(); - _.each(author.groups, group => { - assert.isObject(group.$metadata); - }) - }) + handle.stop(); - done(); - } - }) - }); + _.each(data, (author) => { + assert.isArray(author.groups); - it('Should fetch Inversed One Meta links with $metadata via Subscription', function (done) { - let query = createQuery({ - groups: { - posts: { - title: 1 - } - } + _.each(author.groups, (group) => { + assert.isObject(group.$metadata); + }); }); - let handle = query.subscribe(); - - Tracker.autorun((c) => { - if (handle.ready()) { - c.stop(); - - let data = query.fetch(); - handle.stop(); + done(); + } + }); + }); + + it('Should fetch Inversed One Meta links with $metadata via Subscription', function (done) { + let query = createQuery({ + groups: { + posts: { + title: 1, + }, + }, + }); - _.each(data, group => { - _.each(group.posts, post => { - assert.isObject(post.$metadata); - assert.isDefined(post.$metadata.random); - }) - }); + let handle = query.subscribe(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); - done(); - } - }); - }); + let data = query.fetch(); + handle.stop(); - it('Should fetch Inversed Many Meta links with $metadata via Subscription', function (done) { - let query = createQuery({ - groups: { - authors: { - $options: {limit: 1}, - name: 1 - } - } + _.each(data, (group) => { + _.each(group.posts, (post) => { + assert.isObject(post.$metadata); + assert.isDefined(post.$metadata.random); + }); }); - let handle = query.subscribe(); + done(); + } + }); + }); + + it('Should fetch Inversed Many Meta links with $metadata via Subscription', function (done) { + let query = createQuery({ + groups: { + authors: { + $options: { limit: 1 }, + name: 1, + }, + }, + }); - Tracker.autorun((c) => { - if (handle.ready()) { - c.stop(); + let handle = query.subscribe(); - let data = query.fetch(); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); - _.each(data, group => { - _.each(group.authors, author => { - assert.isObject(author.$metadata); - }); - }); + let data = query.fetch(); - done(); - handle.stop(); - } + _.each(data, (group) => { + _.each(group.authors, (author) => { + assert.isObject(author.$metadata); + }); }); + + done(); + handle.stop(); + } + }); + }); + + it('Should work with promises', async function () { + let query = createQuery({ + groups: { + posts: { + title: 1, + }, + }, }); - it('Should work with promises', async function () { - let query = createQuery({ - groups: { - posts: { - title: 1 - } - } - }); + let result = await query.fetchAsync(); - let result = await query.fetchSync(); + assert.isArray(result); + assert.isTrue(result.length > 0); + result.forEach((item) => { + assert.isArray(item.posts); + assert.isTrue(item.posts.length > 0); + }); - assert.isArray(result); - assert.isTrue(result.length > 0); - result.forEach(item => { - assert.isArray(item.posts); - assert.isTrue(item.posts.length > 0); - }); + result = await query.fetchOneAsync(); - result = await query.fetchOneSync(); + assert.isObject(result); + assert.isString(result._id); + assert.isArray(result.posts); - assert.isObject(result); - assert.isString(result._id); - assert.isArray(result.posts); + result = await query.getCountAsync(); - result = await query.getCountSync(); + assert.isNumber(result); + }); - assert.isNumber(result); + it('Should work with fetchOne', function (done) { + let query = createQuery({ + groups: { + posts: { + title: 1, + }, + }, }); - it('Should work with fetchOne', function (done) { - let query = createQuery({ - groups: { - posts: { - title: 1 - } - } - }); + query.fetchOne((err, group) => { + assert.isNotArray(group); + assert.isObject(group); + assert.isArray(group.posts); - query.fetchOne((err, group) => { - assert.isNotArray(group); - assert.isObject(group); - assert.isArray(group.posts); - - done(); - }) - }) - - it('Should work sorting with options that contain a dot', function () { - let query = createQuery({ - posts: { - author: { - $filter({options}) { - options.sort = { - 'profile.firstName': 1 - } - }, - profile: 1, - } - } - }); - - query.fetch((err, data) => { - assert.isArray(data); - }) + done(); }); - - it('Should properly clone and work with setParams', function () { - let query = createQuery({ - posts: { - $options: {limit: 5} - } - }); - - let clone = query.clone({}); - - assert.isFunction(clone.fetch); - assert.isFunction(clone.fetchOne); - assert.isFunction(clone.setParams); - assert.isFunction(clone.setParams({}).fetchOne); - }); - - it('Should work with denormalized fields in the many links', async () => { - const query = createQuery({ - users: { - $filters: { - name: {$regex: 'User - (2|3)'} - }, - friends: { - name: 1, - friendsCached: { - name: 1 - } - } - } - }); - - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); - - const users = query.fetch(); - - assert.equal(users.length, 2); - users.forEach(user => { - user.friends.forEach(friend => { - // each friend should have defined friendsCached - assert.isArray(friend.friendsCached); - // db cache field should be removed - assert.isUndefined(friend.friendCache); - - friend.friendsCached.forEach(cache => assert.isString(cache.name)); - }); - }); + }); + + it('Should work sorting with options that contain a dot', function () { + let query = createQuery({ + posts: { + author: { + $filter({ options }) { + options.sort = { + 'profile.firstName': 1, + }; + }, + profile: 1, + }, + }, }); - it('Should work securely with reactive queries and linked exposures', function () { + query.fetch((err, data) => { + assert.isArray(data); + }); + }); + it('Should properly clone and work with setParams', function () { + let query = createQuery({ + posts: { + $options: { limit: 5 }, + }, }); - it('Should work with links on nested fields - one', async () => { - const query = createQuery({ - files: { - filename: 1, - project: { - name: 1, - }, - meta: 1, - }, - }); + let clone = query.clone({}); + + assert.isFunction(clone.fetch); + assert.isFunction(clone.fetchOne); + assert.isFunction(clone.setParams); + assert.isFunction(clone.setParams({}).fetchOne); + }); + + it('Should work with denormalized fields in the many links', async () => { + const query = createQuery({ + users: { + $filters: { + name: { $regex: 'User - (2|3)' }, + }, + friends: { + name: 1, + friendsCached: { + name: 1, + }, + }, + }, + }); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + const users = query.fetch(); - const files = query.fetch(); + assert.equal(users.length, 2); + users.forEach((user) => { + user.friends.forEach((friend) => { + // each friend should have defined friendsCached + assert.isArray(friend.friendsCached); + // db cache field should be removed + assert.isUndefined(friend.friendCache); - expect(files).to.be.an('array'); - expect(files).to.have.length(2); - files.forEach(file => { - expect(file.project).to.be.an('object'); - expect(file.project.name).to.be.a('string'); - }); + friend.friendsCached.forEach((cache) => assert.isString(cache.name)); + }); + }); + }); + + it('Should work securely with reactive queries and linked exposures', function () {}); + + it('Should work with links on nested fields - one', async () => { + const query = createQuery({ + files: { + filename: 1, + project: { + name: 1, + }, + meta: 1, + }, }); - it('Should work with links on nested fields inside nested fields - one', async () => { - const query = createQuery({ - files: { - filename: 1, - meta: { - type: 1, - project: { - name: 1, - }, - }, - }, - }); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const files = query.fetch(); - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + expect(files).to.be.an('array'); + expect(files).to.have.length(2); + files.forEach((file) => { + expect(file.project).to.be.an('object'); + expect(file.project.name).to.be.a('string'); + }); + }); + + it('Should work with links on nested fields inside nested fields - one', async () => { + const query = createQuery({ + files: { + filename: 1, + meta: { + type: 1, + project: { + name: 1, + }, + }, + }, + }); - const files = query.fetch(); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - // console.log('files', files); + const files = query.fetch(); - expect(files).to.be.an('array'); - expect(files).to.have.length(2); - files.forEach(file => { - expect(file.meta).to.be.an('object'); - expect(file.meta.project).to.be.an('object'); - expect(file.meta.project.name).to.be.a('string'); - }); + // console.log('files', files); - handle.stop(); + expect(files).to.be.an('array'); + expect(files).to.have.length(2); + files.forEach((file) => { + expect(file.meta).to.be.an('object'); + expect(file.meta.project).to.be.an('object'); + expect(file.meta.project.name).to.be.a('string'); }); - it('Should work with links on nested fields inside nested fields (array) - one', async () => { - const query = createQuery({ - files: { - filename: 1, - metas: { - type: 1, - project: { - name: 1, - }, - }, - }, - }); - - - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + handle.stop(); + }); + + it('Should work with links on nested fields inside nested fields (array) - one', async () => { + const query = createQuery({ + files: { + filename: 1, + metas: { + type: 1, + project: { + name: 1, + }, + }, + }, + }); - const files = query.fetch(); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - expect(files).to.be.an('array'); - expect(files).to.have.length(2); - files.forEach(file => { - expect(file.metas).to.be.an('array'); - expect(file.metas[0].project).to.be.an('object'); - expect(file.metas[0].project.name).to.be.a('string'); - }); + const files = query.fetch(); - handle.stop(); + expect(files).to.be.an('array'); + expect(files).to.have.length(2); + files.forEach((file) => { + expect(file.metas).to.be.an('array'); + expect(file.metas[0].project).to.be.an('object'); + expect(file.metas[0].project.name).to.be.a('string'); }); - it('Should work with links on nested fields inside nested fields - many in object', async () => { - const query = createQuery({ - files: { - filename: 1, - meta: { - type: 1, - manyProjects: { - name: 1, - }, - }, - }, - }); - - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + handle.stop(); + }); + + it('Should work with links on nested fields inside nested fields - many in object', async () => { + const query = createQuery({ + files: { + filename: 1, + meta: { + type: 1, + manyProjects: { + name: 1, + }, + }, + }, + }); - const files = query.fetch(); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - // console.log('files', files); + const files = query.fetch(); - expect(files).to.be.an('array'); - expect(files).to.have.length(2); - files.forEach(file => { - expect(file.meta).to.be.an('object'); - expect(file.meta.manyProjects).to.be.an('array'); - }); + // console.log('files', files); - handle.stop(); + expect(files).to.be.an('array'); + expect(files).to.have.length(2); + files.forEach((file) => { + expect(file.meta).to.be.an('object'); + expect(file.meta.manyProjects).to.be.an('array'); }); - it('Should work with links on nested fields inside nested fields - many in array', async () => { - const query = createQuery({ - files: { - filename: 1, - metas: { - type: 1, - manyProjects: { - name: 1, - }, - }, - }, - }); - - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); - - const files = query.fetch(); - - // console.log('files', files); - - expect(files).to.be.an('array'); - expect(files).to.have.length(2); - files.forEach(file => { - expect(file.metas).to.be.an('array'); - if (file.metas.length > 0) { - expect(file.metas[0].manyProjects).to.be.an('array'); - } - if (file.filename === 'test.txt') { - expect(file.metas[0].manyProjects).to.have.length(1); - expect(file.metas[1].manyProjects).to.have.length(2); - expect(file.metas[0].manyProjects[0].name).to.be.equal('Project 1'); - // expect(file.metas[1].manyProjects[0].name).to.be.equal('Project 2'); - // expect(file.metas[1].manyProjects[1].name).to.be.equal('Project 1'); - } - }); - - handle.stop(); + handle.stop(); + }); + + it('Should work with links on nested fields inside nested fields - many in array', async () => { + const query = createQuery({ + files: { + filename: 1, + metas: { + type: 1, + manyProjects: { + name: 1, + }, + }, + }, }); - it('Should work with links on nested fields - one inversed', async () => { - const query = createQuery({ - projects: { - name: 1, - files: { - filename: 1, - meta: 1, - }, - }, - }); - - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); - - const projects = query.fetch(); - - expect(projects).to.be.an('array'); - expect(projects).to.have.length(2); - projects.forEach(project => { - expect(project.files).to.be.an('array'); - project.files.forEach(file => { - expect(file.filename).to.be.a('string'); - expect(file.meta).to.be.an('object'); - // all keys expected - expect(_.keys(file.meta)).to.have.length(3); - }); - }); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); + + const files = query.fetch(); + + // console.log('files', files); + + expect(files).to.be.an('array'); + expect(files).to.have.length(2); + files.forEach((file) => { + expect(file.metas).to.be.an('array'); + if (file.metas.length > 0) { + expect(file.metas[0].manyProjects).to.be.an('array'); + } + if (file.filename === 'test.txt') { + expect(file.metas[0].manyProjects).to.have.length(1); + expect(file.metas[1].manyProjects).to.have.length(2); + expect(file.metas[0].manyProjects[0].name).to.be.equal('Project 1'); + // expect(file.metas[1].manyProjects[0].name).to.be.equal('Project 2'); + // expect(file.metas[1].manyProjects[1].name).to.be.equal('Project 1'); + } + }); - handle.stop(); + handle.stop(); + }); + + it('Should work with links on nested fields - one inversed', async () => { + const query = createQuery({ + projects: { + name: 1, + files: { + filename: 1, + meta: 1, + }, + }, }); - it('Should work with links on nested fields - one inversed without meta (remove link storages)', async () => { - const query = createQuery({ - projects: { - name: 1, - files: { - filename: 1, - }, - }, - }); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); + + const projects = query.fetch(); + + expect(projects).to.be.an('array'); + expect(projects).to.have.length(2); + projects.forEach((project) => { + expect(project.files).to.be.an('array'); + project.files.forEach((file) => { + expect(file.filename).to.be.a('string'); + expect(file.meta).to.be.an('object'); + // all keys expected + expect(_.keys(file.meta)).to.have.length(3); + }); + }); - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + handle.stop(); + }); + + it('Should work with links on nested fields - one inversed without meta (remove link storages)', async () => { + const query = createQuery({ + projects: { + name: 1, + files: { + filename: 1, + }, + }, + }); - const projects = query.fetch(); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - expect(projects).to.be.an('array'); - expect(projects).to.have.length(2); - projects.forEach(project => { - expect(project.files).to.be.an('array'); - project.files.forEach(file => { - expect(file.filename).to.be.a('string'); - expect(file.meta).to.be.eql({}); - }); - }); + const projects = query.fetch(); - handle.stop(); + expect(projects).to.be.an('array'); + expect(projects).to.have.length(2); + projects.forEach((project) => { + expect(project.files).to.be.an('array'); + project.files.forEach((file) => { + expect(file.filename).to.be.a('string'); + expect(file.meta).to.be.eql({}); + }); }); - it('Should work with links on nested fields - many', async () => { - const query = createQuery({ - files: { - filename: 1, - projects: { - name: 1, - }, - }, - }); - - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + handle.stop(); + }); + + it('Should work with links on nested fields - many', async () => { + const query = createQuery({ + files: { + filename: 1, + projects: { + name: 1, + }, + }, + }); - const files = query.fetch(); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - expect(files).to.be.an('array'); - expect(files).to.have.length(2); - files.forEach(file => { - expect(file.projects).to.be.an('array'); - expect(file.projects.length).to.be.gt(0); - file.projects.forEach(project => { - expect(project.name).to.be.a('string'); - }); - }); + const files = query.fetch(); - handle.stop(); + expect(files).to.be.an('array'); + expect(files).to.have.length(2); + files.forEach((file) => { + expect(file.projects).to.be.an('array'); + expect(file.projects.length).to.be.gt(0); + file.projects.forEach((project) => { + expect(project.name).to.be.a('string'); + }); }); - it('Should work with links on nested fields - many inversed', async () => { - const query = createQuery({ - projects: { - name: 1, - filesMany: { - filename: 1, - metas: { - type: 1, - }, - }, - }, - }); - + handle.stop(); + }); + + it('Should work with links on nested fields - many inversed', async () => { + const query = createQuery({ + projects: { + name: 1, + filesMany: { + filename: 1, + metas: { + type: 1, + }, + }, + }, + }); - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - const projects = query.fetch(); + const projects = query.fetch(); - expect(projects).to.be.an('array'); - expect(projects).to.have.length(2); - projects.forEach(project => { - expect(project.filesMany).to.be.an('array'); - expect(project.filesMany.length).to.be.gt(0); - project.filesMany.forEach(file => { - expect(file.filename).to.be.a('string'); - expect(file.metas).to.be.an('array'); - expect(file.metas).to.have.length.gt(0); + expect(projects).to.be.an('array'); + expect(projects).to.have.length(2); + projects.forEach((project) => { + expect(project.filesMany).to.be.an('array'); + expect(project.filesMany.length).to.be.gt(0); + project.filesMany.forEach((file) => { + expect(file.filename).to.be.a('string'); + expect(file.metas).to.be.an('array'); + expect(file.metas).to.have.length.gt(0); - file.metas.forEach(meta => { - // only type - expect(_.keys(meta)).to.be.eql(['type']); - }); - }); + file.metas.forEach((meta) => { + // only type + expect(_.keys(meta)).to.be.eql(['type']); }); + }); + }); + }); + + it('Should work with links on nested fields - many inversed without meta (remove link storage)', async () => { + const query = createQuery({ + projects: { + name: 1, + filesMany: { + filename: 1, + }, + }, }); - it('Should work with links on nested fields - many inversed without meta (remove link storage)', async () => { - const query = createQuery({ - projects: { - name: 1, - filesMany: { - filename: 1, - }, - }, - }); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const projects = query.fetch(); - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); - - const projects = query.fetch(); - - expect(projects).to.be.an('array'); - expect(projects).to.have.length(2); - projects.forEach(project => { - expect(project.filesMany).to.be.an('array'); - expect(project.filesMany.length).to.be.gt(0); - project.filesMany.forEach(file => { - expect(file.filename).to.be.a('string'); - expect(file.metas).to.be.an('array'); - expect(file.metas.length).to.be.gt(0); - file.metas.forEach(meta => { - expect(meta).to.be.eql({}); - }); - }); + expect(projects).to.be.an('array'); + expect(projects).to.have.length(2); + projects.forEach((project) => { + expect(project.filesMany).to.be.an('array'); + expect(project.filesMany.length).to.be.gt(0); + project.filesMany.forEach((file) => { + expect(file.filename).to.be.a('string'); + expect(file.metas).to.be.an('array'); + expect(file.metas.length).to.be.gt(0); + file.metas.forEach((meta) => { + expect(meta).to.be.eql({}); }); + }); + }); + }); + + it('Should work with foreignIdentityField - one', async function () { + const query = createQuery({ + products: { + $filters: { + singleProductId: 1, + }, + singleAttribute: { + singleProductId: 1, + delivery: 1, + }, + }, }); - it('Should work with foreignIdentityField - one', async function () { - const query = createQuery({ - products: { - $filters: { - singleProductId: 1, - }, - singleAttribute: { - singleProductId: 1, - delivery: 1, - }, - }, - }); - - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - const products = query.fetch(); - expect(products).to.have.length(1); - products.forEach((product) => { - expect(product.singleAttribute).to.be.an('object'); - expect(product.singleAttribute.delivery).to.be.equal(12); - expect(product.singleAttribute.singleProductId).to.be.equal(1); - }); + const products = query.fetch(); + expect(products).to.have.length(1); + products.forEach((product) => { + expect(product.singleAttribute).to.be.an('object'); + expect(product.singleAttribute.delivery).to.be.equal(12); + expect(product.singleAttribute.singleProductId).to.be.equal(1); + }); + }); + + it('Should work with foreignIdentityField - one inversed', async function () { + const query = createQuery({ + product_attributes: { + $filters: { + singleProductId: 1, + }, + singleProduct: { + price: 1, + }, + }, }); - - it('Should work with foreignIdentityField - one inversed', async function () { - const query = createQuery({ - product_attributes: { - $filters: { - singleProductId: 1, - }, - singleProduct: { - price: 1, - }, - }, - }); - - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - const products = query.fetch(); - expect(products).to.have.length(1); - products.forEach((product) => { - expect(product.singleProduct).to.be.an('object'); - expect(product.singleProduct.price).to.be.equal(1300); - }); + const products = query.fetch(); + expect(products).to.have.length(1); + products.forEach((product) => { + expect(product.singleProduct).to.be.an('object'); + expect(product.singleProduct.price).to.be.equal(1300); + }); + }); + + it('Should work with foreignIdentityField - many', async function () { + const query = createQuery({ + product_attributes: { + $filters: { + productId: { $ne: null }, + }, + $options: { + sort: { + // be sure to get productId=1 first + productId: 1, + }, + }, + productId: 1, + products: { + _id: 1, + productId: 1, + }, + }, }); - it('Should work with foreignIdentityField - many', async function () { - const query = createQuery({ - product_attributes: { - $filters: { - productId: {$ne: null}, - }, - products: { - _id: 1, - productId: 1, - }, - }, - }); - - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); - - const attributes = query.fetch(); - expect(attributes).to.have.length(2); - // see fixtures.js - expect(attributes[0].products).to.have.length(2); - expect(attributes[1].products).to.have.length(1); - }); - - it('Should work with foreignIdentityField - many inversed', async function () { - const query = createQuery({ - products: { - $filters: { - productId: {$ne: null}, - }, - productId: 1, - attributes: { - _id: 1, - productId: 1, - }, - }, - }); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); + + const attributes = query.fetch(); + expect(attributes).to.have.length(2); + // see fixtures.js + expect(attributes[0].products).to.have.length(2); + expect(attributes[1].products).to.have.length(1); + }); + + it('Should work with foreignIdentityField - many inversed', async function () { + const query = createQuery({ + products: { + $filters: { + productId: { $ne: null }, + }, + productId: 1, + attributes: { + _id: 1, + productId: 1, + }, + }, + }); - const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + const handle = query.subscribe(); + await waitForHandleToBeReady(handle); - const products = query.fetch(); - expect(products).to.have.length(3); - expect(products[0].attributes).to.have.length(1); - expect(products[1].attributes).to.have.length(1); - expect(products[2].attributes).to.have.length(1); - }); + const products = query.fetch(); + expect(products).to.have.length(3); + expect(products[0].attributes).to.have.length(1); + expect(products[1].attributes).to.have.length(1); + expect(products[2].attributes).to.have.length(1); + }); }); diff --git a/lib/query/testing/link-cache/collections.js b/lib/query/testing/link-cache/collections.js index b163564a..3ee33dfc 100644 --- a/lib/query/testing/link-cache/collections.js +++ b/lib/query/testing/link-cache/collections.js @@ -1,117 +1,115 @@ -import {Mongo} from 'meteor/mongo'; - export const Authors = new Mongo.Collection('cache_authors'); export const AuthorProfiles = new Mongo.Collection('cache_author_profiles'); export const Posts = new Mongo.Collection('cache_posts'); export const Groups = new Mongo.Collection('cache_groups'); export const Categories = new Mongo.Collection('cache_categories'); -Authors.remove({}); -AuthorProfiles.remove({}); -Posts.remove({}); -Groups.remove({}); -Categories.remove({}); +await Authors.removeAsync({}); +await AuthorProfiles.removeAsync({}); +await Posts.removeAsync({}); +await Groups.removeAsync({}); +await Categories.removeAsync({}); Posts.addLinks({ - author: { - type: 'one', - collection: Authors, - field: 'authorId', - denormalize: { - field: 'authorCache', - body: { - name: 1, - address: 1, - } - } + author: { + type: 'one', + collection: Authors, + field: 'authorId', + denormalize: { + field: 'authorCache', + body: { + name: 1, + address: 1, + }, + }, + }, + categories: { + type: 'many', + metadata: true, + collection: Categories, + field: 'categoryIds', + denormalize: { + field: 'categoriesCache', + body: { + name: 1, + }, }, - categories: { - type: 'many', - metadata: true, - collection: Categories, - field: 'categoryIds', - denormalize: { - field: 'categoriesCache', - body: { - name: 1, - } - } - } + }, }); Authors.addLinks({ - posts: { - collection: Posts, - inversedBy: 'author', - denormalize: { - field: 'postCache', - body: { - title: 1, - } - } + posts: { + collection: Posts, + inversedBy: 'author', + denormalize: { + field: 'postCache', + body: { + title: 1, + }, + }, + }, + groups: { + type: 'many', + collection: Groups, + field: 'groupIds', + denormalize: { + field: 'groupsCache', + body: { + name: 1, + }, }, - groups: { - type: 'many', - collection: Groups, - field: 'groupIds', - denormalize: { - field: 'groupsCache', - body: { - name: 1, - } - } + }, + profile: { + type: 'one', + metadata: true, + collection: AuthorProfiles, + field: 'profileId', + unique: true, + denormalize: { + field: 'profileCache', + body: { + name: 1, + }, }, - profile: { - type: 'one', - metadata: true, - collection: AuthorProfiles, - field: 'profileId', - unique: true, - denormalize: { - field: 'profileCache', - body: { - name: 1, - } - } - } + }, }); AuthorProfiles.addLinks({ - author: { - collection: Authors, - inversedBy: 'profile', - unique: true, - denormalize: { - field: 'authorCache', - body: { - name: 1, - } - } - } + author: { + collection: Authors, + inversedBy: 'profile', + unique: true, + denormalize: { + field: 'authorCache', + body: { + name: 1, + }, + }, + }, }); Groups.addLinks({ - authors: { - collection: Authors, - inversedBy: 'groups', - denormalize: { - field: 'authorsCache', - body: { - name: 1, - } - } - } + authors: { + collection: Authors, + inversedBy: 'groups', + denormalize: { + field: 'authorsCache', + body: { + name: 1, + }, + }, + }, }); Categories.addLinks({ - posts: { - collection: Posts, - inversedBy: 'categories', - denormalize: { - field: 'postsCache', - body: { - title: 1, - } - } - } + posts: { + collection: Posts, + inversedBy: 'categories', + denormalize: { + field: 'postsCache', + body: { + title: 1, + }, + }, + }, }); diff --git a/lib/query/testing/link-cache/fixtures.js b/lib/query/testing/link-cache/fixtures.js index 0972799e..74b2e7d8 100755 --- a/lib/query/testing/link-cache/fixtures.js +++ b/lib/query/testing/link-cache/fixtures.js @@ -1,4 +1,11 @@ -import {Authors, Groups, Posts, Categories, AuthorProfiles} from './collections'; +import { _ } from 'meteor/underscore'; +import { + Authors, + Groups, + Posts, + Categories, + AuthorProfiles, +} from './collections'; const GROUPS = 3; const CATEGORIES = 3; @@ -10,68 +17,68 @@ export let groupIds = []; export let authorIds = []; export let postIds = []; -export default function createFixtures() { - for (let i = 0; i < CATEGORIES; i++) { - const categoryId = Categories.insert({ - name: `Category ${i}` - }); +export default async function createFixtures() { + for await (const i of _.range(CATEGORIES)) { + const categoryId = await Categories.insertAsync({ + name: `Category ${i}`, + }); - categoryIds.push(categoryId); - } + categoryIds.push(categoryId); + } + for await (const i of _.range(GROUPS)) { + const groupId = await Groups.insertAsync({ + name: `Group ${i}`, + }); - for (let i = 0; i < GROUPS; i++) { - const groupId = Groups.insert({ - name: `Group ${i}` - }); + groupIds.push(groupId); + } - groupIds.push(groupId); - } - - groupIds.forEach(groupId => { - for (let i = 0; i < AUTHOR_PER_GROUPS; i++) { - const authorId = Authors.insert({ - name: `Author ${authorIds.length}`, - createdAt: new Date(), - }); + for await (const groupId of groupIds) { + for await (const i of _.range(AUTHOR_PER_GROUPS)) { + const authorId = await Authors.insertAsync({ + name: `Author ${authorIds.length}`, + createdAt: new Date(), + }); - const authorProfileId = AuthorProfiles.insert({ - name: `Author ${authorIds.length}`, - createdAt: new Date(), - }); + const authorProfileId = await AuthorProfiles.insertAsync({ + name: `Author ${authorIds.length}`, + createdAt: new Date(), + }); - Authors.getLink(authorId, 'profile').set(authorProfileId); + await (await Authors.getLink(authorId, 'profile')).set(authorProfileId); - authorIds.push(authorId); + authorIds.push(authorId); - // link it to the group - const groupLink = Authors.getLink(authorId, 'groups'); - groupLink.add(groupId); + // link it to the group + const groupLink = await Authors.getLink(authorId, 'groups'); + groupLink.add(groupId); - for (let j = 0; j < POSTS_PER_AUTHOR; j++) { - createPost(authorId); - } - } - }); + for await (const j of _.range(POSTS_PER_AUTHOR)) { + await createPost(authorId); + } + } + } } -function createPost(authorId) { - const postId = Posts.insert({ - title: `Post ${postIds.length}`, - createdAt: new Date(), - }); +async function createPost(authorId) { + const postId = await Posts.insertAsync({ + title: `Post ${postIds.length}`, + createdAt: new Date(), + }); - postIds.push(postId); + postIds.push(postId); - const authorLink = Posts.getLink(postId, 'author'); - authorLink.set(authorId); + const authorLink = await Posts.getLink(postId, 'author'); + await authorLink.set(authorId); - const randomCategoryId = categoryIds[Math.floor(Math.random()*categoryIds.length)]; + const randomCategoryId = + categoryIds[Math.floor(Math.random() * categoryIds.length)]; - const categoriesLink = Posts.getLink(postId, 'categories'); - categoriesLink.add(randomCategoryId, { - createdAt: new Date(), - }); + const categoriesLink = await Posts.getLink(postId, 'categories'); + await categoriesLink.add(randomCategoryId, { + createdAt: new Date(), + }); - return postId; + return postId; } diff --git a/lib/query/testing/link-cache/server.test.js b/lib/query/testing/link-cache/server.test.js index e8f1aff1..28324d95 100755 --- a/lib/query/testing/link-cache/server.test.js +++ b/lib/query/testing/link-cache/server.test.js @@ -1,403 +1,402 @@ import { assert } from 'chai'; import createFixtures from './fixtures'; -import { createQuery } from 'meteor/cultofcoders:grapher'; import { - Authors, - AuthorProfiles, - Groups, - Posts, - Categories, + Authors, + AuthorProfiles, + Groups, + Posts, + Categories, } from './collections'; -describe('Query Link Denormalization', function() { - // increase this before for some reason Mongo 5 takes longer - // to initiate the fixtures on the before hook - this.timeout(1000 * 5); - - before(function(done) { - createFixtures(); - done(); - }) - - it('Should not cache work with nested options', function() { - let query = Posts.createQuery({ - $options: { limit: 5 }, - author: { - $options: { limit: 1 }, - name: 1, - }, - }); - - let insideFind = false; - stubFind(Authors, function() { - insideFind = true; - }); - - query.fetch(); - assert.isTrue(insideFind); - }); - - it('Should work properly - One Direct', function() { - let query = Posts.createQuery({ - $options: { limit: 5 }, - author: { - name: 1, - }, - }); - - let insideFind = false; - stubFind(Authors, function() { - insideFind = true; - }); - - // when fetching, Authors.find() should not be called - let post = query.fetchOne(); - - assert.isFalse(insideFind); - assert.isObject(post.author); - assert.isString(post.author._id); - - unstubFind(Authors); - - // now that we specify an additional field, it should bypass the cache - query = Posts.createQuery({ - author: { - name: 1, - createdAt: 1, - }, - }); - - insideFind = false; - stubFind(Authors, function() { - insideFind = true; - }); - - query.fetch(); - assert.isTrue(insideFind); - - unstubFind(Authors); - }); - - it('Should work properly - One Inversed', function() { - let query = Authors.createQuery({ - $options: { limit: 2 }, - posts: { - title: 1, - }, - }); - - let insideFind = false; - stubFind(Posts, function() { - insideFind = true; - }); - - let author = query.fetchOne(); - - assert.isFalse(insideFind); - assert.isArray(author.posts); - assert.isObject(author.posts[0]); - assert.isString(author.posts[0].title); - assert.isString(author.posts[0]._id); - - unstubFind(Posts); - - // now that we specify an additional field, it should bypass the cache - query = Authors.createQuery({ - $options: { limit: 2 }, - posts: { - title: 1, - createdAt: 1, - }, - }); - - insideFind = false; - stubFind(Posts, function() { - insideFind = true; - }); - - query.fetch(); - assert.isTrue(insideFind); - - unstubFind(Posts); - }); - - it('Should work properly - One Meta Direct', function() { - // console.log(Authors.find().fetch()); - - let query = Authors.createQuery({ - $options: { limit: 5 }, - profile: { - name: 1, - }, - }); - - let insideFind = false; - stubFind(AuthorProfiles, function() { - insideFind = true; - }); - - let author = query.fetchOne(); - - assert.isFalse(insideFind); - assert.isObject(author.profile); - assert.isString(author.profile._id); - - unstubFind(AuthorProfiles); - - // now that we specify an additional field, it should bypass the cache - query = Authors.createQuery({ - $options: { limit: 5 }, - profile: { - name: 1, - createdAt: 1, - }, - }); - - insideFind = false; - stubFind(AuthorProfiles, function() { - insideFind = true; - }); - - query.fetch(); - assert.isTrue(insideFind); - - unstubFind(AuthorProfiles); - }); - - it('Should work properly - One Meta Inversed', function() { - let query = AuthorProfiles.createQuery({ - $options: { limit: 5 }, - author: { - name: 1, - }, - }); +describe('Query Link Denormalization', function () { + // increase this before for some reason Mongo 5 takes longer + // to initiate the fixtures on the before hook + this.timeout(1000 * 5); + + before(async function (done) { + await createFixtures(); + done(); + }); + + it('Should not cache work with nested options', async function () { + let query = Posts.createQuery({ + $options: { limit: 5 }, + author: { + $options: { limit: 1 }, + name: 1, + }, + }); + + let insideFind = false; + stubFind(Authors, function () { + insideFind = true; + }); + + await query.fetchAsync(); + assert.isTrue(insideFind); + }); + + it('Should work properly - One Direct', async function () { + let query = Posts.createQuery({ + $options: { limit: 5 }, + author: { + name: 1, + }, + }); + + let insideFind = false; + stubFind(Authors, function () { + insideFind = true; + }); + + // when fetching, Authors.find() should not be called + let post = await query.fetchOneAsync(); + + assert.isFalse(insideFind); + assert.isObject(post.author); + assert.isString(post.author._id); + + unstubFind(Authors); + + // now that we specify an additional field, it should bypass the cache + query = Posts.createQuery({ + author: { + name: 1, + createdAt: 1, + }, + }); + + insideFind = false; + stubFind(Authors, function () { + insideFind = true; + }); + + await query.fetchAsync(); + assert.isTrue(insideFind); + + unstubFind(Authors); + }); + + it('Should work properly - One Inversed', async function () { + let query = Authors.createQuery({ + $options: { limit: 2 }, + posts: { + title: 1, + }, + }); + + let insideFind = false; + stubFind(Posts, function () { + insideFind = true; + }); + + let author = await query.fetchOneAsync(); + + assert.isFalse(insideFind); + assert.isArray(author.posts); + assert.isObject(author.posts[0]); + assert.isString(author.posts[0].title); + assert.isString(author.posts[0]._id); + + unstubFind(Posts); + + // now that we specify an additional field, it should bypass the cache + query = Authors.createQuery({ + $options: { limit: 2 }, + posts: { + title: 1, + createdAt: 1, + }, + }); + + insideFind = false; + stubFind(Posts, function () { + insideFind = true; + }); + + await query.fetchAsync(); + assert.isTrue(insideFind); + + unstubFind(Posts); + }); + + it('Should work properly - One Meta Direct', async function () { + // console.log(Authors.find().fetch()); + + let query = Authors.createQuery({ + $options: { limit: 5 }, + profile: { + name: 1, + }, + }); + + let insideFind = false; + stubFind(AuthorProfiles, function () { + insideFind = true; + }); + + let author = await query.fetchOneAsync(); + + assert.isFalse(insideFind); + assert.isObject(author.profile); + assert.isString(author.profile._id); + + unstubFind(AuthorProfiles); + + // now that we specify an additional field, it should bypass the cache + query = Authors.createQuery({ + $options: { limit: 5 }, + profile: { + name: 1, + createdAt: 1, + }, + }); + + insideFind = false; + stubFind(AuthorProfiles, function () { + insideFind = true; + }); + + await query.fetchAsync(); + assert.isTrue(insideFind); + + unstubFind(AuthorProfiles); + }); + + it('Should work properly - One Meta Inversed', async function () { + let query = AuthorProfiles.createQuery({ + $options: { limit: 5 }, + author: { + name: 1, + }, + }); + + let insideFind = false; + stubFind(Authors, function () { + insideFind = true; + }); - let insideFind = false; - stubFind(Authors, function() { - insideFind = true; - }); - - let profile = query.fetchOne(); - - assert.isFalse(insideFind); - assert.isObject(profile.author); - assert.isString(profile.author._id); - - unstubFind(Authors); - - // now that we specify an additional field, it should bypass the cache - query = AuthorProfiles.createQuery({ - $options: { limit: 5 }, - author: { - name: 1, - createdAt: 1, - }, - }); - - insideFind = false; - stubFind(Authors, function() { - insideFind = true; - }); + let profile = await query.fetchOneAsync(); - query.fetch(); - assert.isTrue(insideFind); - - unstubFind(Authors); - }); - - it('Should work properly - Many Direct', function() { - let query = Authors.createQuery({ - $options: { limit: 5 }, - groups: { - name: 1, - }, - }); + assert.isFalse(insideFind); + assert.isObject(profile.author); + assert.isString(profile.author._id); - let insideFind = false; - stubFind(Groups, function() { - insideFind = true; - }); + unstubFind(Authors); - let author = query.fetchOne(); - - assert.isFalse(insideFind); - assert.isArray(author.groups); - assert.isObject(author.groups[0]); - assert.isString(author.groups[0].name); - assert.isString(author.groups[0]._id); - - unstubFind(Groups); - - query = Authors.createQuery({ - $options: { limit: 5 }, - groups: { - name: 1, - createdAt: 1, - }, - }); + // now that we specify an additional field, it should bypass the cache + query = AuthorProfiles.createQuery({ + $options: { limit: 5 }, + author: { + name: 1, + createdAt: 1, + }, + }); + + insideFind = false; + stubFind(Authors, function () { + insideFind = true; + }); + + await query.fetchAsync(); + assert.isTrue(insideFind); + + unstubFind(Authors); + }); + + it('Should work properly - Many Direct', async function () { + let query = Authors.createQuery({ + $options: { limit: 5 }, + groups: { + name: 1, + }, + }); + + let insideFind = false; + stubFind(Groups, function () { + insideFind = true; + }); + + let author = await query.fetchOneAsync(); + + assert.isFalse(insideFind); + assert.isArray(author.groups); + assert.isObject(author.groups[0]); + assert.isString(author.groups[0].name); + assert.isString(author.groups[0]._id); + + unstubFind(Groups); + + query = Authors.createQuery({ + $options: { limit: 5 }, + groups: { + name: 1, + createdAt: 1, + }, + }); + + insideFind = false; + stubFind(Groups, function () { + insideFind = true; + }); + + await query.fetchAsync(); + assert.isTrue(insideFind); + + unstubFind(Groups); + }); + + it('Should work properly - Many Inversed', async function () { + let query = Groups.createQuery({ + $options: { limit: 5 }, + authors: { + name: 1, + }, + }); + + let insideFind = false; + stubFind(Authors, function () { + insideFind = true; + }); + + let group = await query.fetchOneAsync(); + + assert.isFalse(insideFind); + assert.isArray(group.authors); + assert.isObject(group.authors[0]); + assert.isString(group.authors[0].name); + assert.isString(group.authors[0]._id); - insideFind = false; - stubFind(Groups, function() { - insideFind = true; - }); - - query.fetch(); - assert.isTrue(insideFind); - - unstubFind(Groups); - }); - - it('Should work properly - Many Inversed', function() { - let query = Groups.createQuery({ - $options: { limit: 5 }, - authors: { - name: 1, - }, - }); - - let insideFind = false; - stubFind(Authors, function() { - insideFind = true; - }); - - let group = query.fetchOne(); - - assert.isFalse(insideFind); - assert.isArray(group.authors); - assert.isObject(group.authors[0]); - assert.isString(group.authors[0].name); - assert.isString(group.authors[0]._id); - - unstubFind(Authors); - - query = Groups.createQuery({ - $options: { limit: 5 }, - authors: { - name: 1, - createdAt: 1, - }, - }); - - insideFind = false; - stubFind(Authors, function() { - insideFind = true; - }); - - query.fetch(); - assert.isTrue(insideFind); - - unstubFind(Authors); - }); - - it('Should work properly - Many Meta Direct', function() { - // console.log(Posts.find({}, {limit: 2}).fetch()); - - let query = Posts.createQuery({ - $options: { limit: 5 }, - categories: { - name: 1, - }, - }); + unstubFind(Authors); - let insideFind = false; - stubFind(Categories, function() { - insideFind = true; - }); - - // when fetching, Authors.find() should not be called - let post = query.fetchOne(); - - assert.isFalse(insideFind); - assert.isArray(post.categories); - assert.isObject(post.categories[0]); - assert.isString(post.categories[0]._id); - - unstubFind(Categories); - - // now that we specify an additional field, it should bypass the cache - query = Posts.createQuery({ - categories: { - name: 1, - createdAt: 1, - }, - }); - - insideFind = false; - stubFind(Categories, function() { - insideFind = true; - }); - - query.fetch(); - assert.isTrue(insideFind); - - unstubFind(Categories); - }); + query = Groups.createQuery({ + $options: { limit: 5 }, + authors: { + name: 1, + createdAt: 1, + }, + }); + + insideFind = false; + stubFind(Authors, function () { + insideFind = true; + }); + + await query.fetchAsync(); + assert.isTrue(insideFind); + + unstubFind(Authors); + }); + + it('Should work properly - Many Meta Direct', async function () { + // console.log(Posts.find({}, {limit: 2}).fetch()); + + let query = Posts.createQuery({ + $options: { limit: 5 }, + categories: { + name: 1, + }, + }); + + let insideFind = false; + stubFind(Categories, function () { + insideFind = true; + }); + + // when fetching, Authors.find() should not be called + let post = await query.fetchOneAsync(); + + assert.isFalse(insideFind); + assert.isArray(post.categories); + assert.isObject(post.categories[0]); + assert.isString(post.categories[0]._id); - it('Should work properly - Many Meta Inversed', function() { - let query = Categories.createQuery({ - $options: { limit: 2 }, - posts: { - title: 1, - }, - }); + unstubFind(Categories); - let insideFind = false; - stubFind(Posts, function() { - insideFind = true; - }); + // now that we specify an additional field, it should bypass the cache + query = Posts.createQuery({ + categories: { + name: 1, + createdAt: 1, + }, + }); + + insideFind = false; + stubFind(Categories, function () { + insideFind = true; + }); + + await query.fetchAsync(); + assert.isTrue(insideFind); + + unstubFind(Categories); + }); + + it('Should work properly - Many Meta Inversed', async function () { + let query = Categories.createQuery({ + $options: { limit: 2 }, + posts: { + title: 1, + }, + }); + + let insideFind = false; + stubFind(Posts, function () { + insideFind = true; + }); - let category = query.fetchOne(); - - assert.isFalse(insideFind); - assert.isArray(category.posts); - assert.isObject(category.posts[0]); - assert.isString(category.posts[0].title); - assert.isString(category.posts[0]._id); + let category = await query.fetchOneAsync(); - unstubFind(Posts); - - // now that we specify an additional field, it should bypass the cache - query = Categories.createQuery({ - $options: { limit: 2 }, - posts: { - title: 1, - createdAt: 1, - }, - }); + assert.isFalse(insideFind); + assert.isArray(category.posts); + assert.isObject(category.posts[0]); + assert.isString(category.posts[0].title); + assert.isString(category.posts[0]._id); - insideFind = false; - stubFind(Posts, function() { - insideFind = true; - }); + unstubFind(Posts); - query.fetch(); - assert.isTrue(insideFind); + // now that we specify an additional field, it should bypass the cache + query = Categories.createQuery({ + $options: { limit: 2 }, + posts: { + title: 1, + createdAt: 1, + }, + }); - unstubFind(Posts); + insideFind = false; + stubFind(Posts, function () { + insideFind = true; }); + + await query.fetchAsync(); + assert.isTrue(insideFind); + + unstubFind(Posts); + }); }); function stubFind(collection, callback) { - if (!collection.oldFind) { - collection.oldFind = collection.find.bind(collection); - collection.oldAggregate = collection.aggregate.bind(collection); - } - - collection.find = function() { - callback(); - return this.oldFind.apply(collection, arguments); - }.bind(collection); - - collection.aggregate = function() { - callback(); - return this.oldAggregate.apply(collection, arguments); - }.bind(collection); + if (!collection.oldFind) { + collection.oldFind = collection.find.bind(collection); + collection.oldAggregate = collection.aggregate.bind(collection); + } + + collection.find = function () { + callback(); + return this.oldFind.apply(collection, arguments); + }.bind(collection); + + collection.aggregate = function () { + callback(); + return this.oldAggregate.apply(collection, arguments); + }.bind(collection); } function unstubFind(collection) { - collection.find = collection.oldFind.bind(collection); - collection.aggregate = collection.oldAggregate.bind(collection); + collection.find = collection.oldFind.bind(collection); + collection.aggregate = collection.oldAggregate.bind(collection); - delete collection.oldFind; - delete collection.oldAggregate; + delete collection.oldFind; + delete collection.oldAggregate; } diff --git a/lib/query/testing/metaFilters.server.test.js b/lib/query/testing/metaFilters.server.test.js index 7c89a7c8..2e4f04d9 100755 --- a/lib/query/testing/metaFilters.server.test.js +++ b/lib/query/testing/metaFilters.server.test.js @@ -2,190 +2,190 @@ import { assert } from 'chai'; import { createQuery } from 'meteor/cultofcoders:grapher'; describe('Hypernova - $meta filters', function () { - it('Should work with $meta filters - One Meta Direct', function () { - const data = createQuery({ - posts: { - group: { - name: 1 - } - } - }).fetch(); - - let post = data[0]; - - const random = post.group.$metadata.random; - - let posts = createQuery({ - posts: { - $filters: {_id: post._id}, - group: { - $filters: { - $meta: {random} - } - } - } - }).fetch(); - - assert.lengthOf(posts, 1); - assert.isObject(posts[0].group); - - posts = createQuery({ - posts: { - $filters: {_id: post._id}, - group: { - $filters: { - $meta: {random: random + 'invalidate'} - } - } - } - }).fetch(); - - assert.lengthOf(posts, 1); - assert.isUndefined(posts[0].group); + it('Should work with $meta filters - One Meta Direct', async function () { + const data = await createQuery({ + posts: { + group: { + name: 1, + }, + }, + }).fetchAsync(); + + let post = data[0]; + + const random = post.group.$metadata.random; + + let posts = await createQuery({ + posts: { + $filters: { _id: post._id }, + group: { + $filters: { + $meta: { random }, + }, + }, + }, + }).fetchAsync(); + + assert.lengthOf(posts, 1); + assert.isObject(posts[0].group); + + posts = await createQuery({ + posts: { + $filters: { _id: post._id }, + group: { + $filters: { + $meta: { random: random + 'invalidate' }, + }, + }, + }, + }).fetchAsync(); + + assert.lengthOf(posts, 1); + assert.isUndefined(posts[0].group); + }); + + it('Should work with $meta filters - One Meta Virtual', async function () { + const data = await createQuery({ + groups: { + posts: { + name: 1, + }, + }, + }).fetchAsync(); + + let group = data[0]; + let post = group.posts[0]; + const random = post.$metadata.random; + assert.isDefined(random); + + let groups = await createQuery({ + groups: { + $filters: { _id: group._id }, + posts: { + $filters: { + _id: post._id, + $meta: { random }, + }, + name: 1, + }, + }, + }).fetchAsync(); + + assert.lengthOf(groups, 1); + assert.lengthOf(groups[0].posts, 1); + assert.isObject(groups[0].posts[0].$metadata); + assert.equal(groups[0].posts[0].$metadata.random, random); + + groups = await createQuery({ + groups: { + $filters: { _id: group._id }, + posts: { + $filters: { + _id: post._id, + $meta: { random: random + 'invalidate' }, + }, + name: 1, + }, + }, + }).fetchAsync(); + + assert.lengthOf(groups, 1); + assert.isTrue(!groups[0].posts || groups[0].posts.length === 0); + }); + + it('Should work with $meta filters - Many Meta Direct', async function () { + let data = await createQuery({ + authors: { + name: 1, + groups: { + $filters: { + $meta: { isAdmin: true }, + }, + $options: { limit: 1 }, + name: 1, + }, + }, + }).fetchAsync(); + + let assertions = 0; + + _.each(data, (author) => { + _.each(author.groups, (group) => { + assert.isObject(group.$metadata); + assert.isTrue(group.$metadata.isAdmin); + assertions++; + }); }); - it('Should work with $meta filters - One Meta Virtual', function () { - const data = createQuery({ - groups: { - posts: { - name: 1 - } - } - }).fetch(); - - let group = data[0]; - let post = group.posts[0]; - const random = post.$metadata.random; - assert.isDefined(random); - - let groups = createQuery({ - groups: { - $filters: {_id: group._id}, - posts: { - $filters: { - _id: post._id, - $meta: {random} - }, - name: 1 - } - } - }).fetch(); - - assert.lengthOf(groups, 1); - assert.lengthOf(groups[0].posts, 1); - assert.isObject(groups[0].posts[0].$metadata); - assert.equal(groups[0].posts[0].$metadata.random, random); - - groups = createQuery({ - groups: { - $filters: {_id: group._id}, - posts: { - $filters: { - _id: post._id, - $meta: {random: random + 'invalidate'} - }, - name: 1 - } - } - }).fetch(); - - assert.lengthOf(groups, 1); - assert.isTrue(!groups[0].posts || groups[0].posts.length === 0); + data = await createQuery({ + authors: { + name: 1, + groups: { + $filters: { + $meta: { isAdmin: false }, + }, + $options: { limit: 1 }, + name: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (author) => { + _.each(author.groups, (group) => { + assert.isObject(group.$metadata); + assert.isFalse(group.$metadata.isAdmin); + assertions++; + }); }); - it('Should work with $meta filters - Many Meta Direct', function () { - let data = createQuery({ - authors: { - name: 1, - groups: { - $filters: { - $meta: {isAdmin: true}, - }, - $options: {limit: 1}, - name: 1 - } - } - }).fetch(); - - let assertions = 0; - - _.each(data, author => { - _.each(author.groups, group => { - assert.isObject(group.$metadata); - assert.isTrue(group.$metadata.isAdmin); - assertions++; - }) - }); - - data = createQuery({ - authors: { - name: 1, - groups: { - $filters: { - $meta: {isAdmin: false}, - }, - $options: {limit: 1}, - name: 1 - } - } - }).fetch(); - - _.each(data, author => { - _.each(author.groups, group => { - assert.isObject(group.$metadata); - assert.isFalse(group.$metadata.isAdmin); - assertions++; - }) - }); - - assert.isTrue(assertions > 0); + assert.isTrue(assertions > 0); + }); + + it('Should work with $meta filters - Many Meta Virtual', async function () { + let data = await createQuery({ + groups: { + name: 1, + authors: { + $filters: { + $meta: { isAdmin: true }, + }, + $options: { limit: 1 }, + name: 1, + }, + }, + }).fetchAsync(); + + let assertions = 0; + + _.each(data, (group) => { + _.each(group.authors, (author) => { + assert.isObject(author.$metadata); + assert.isTrue(author.$metadata.isAdmin); + assertions++; + }); }); - it('Should work with $meta filters - Many Meta Virtual', function () { - let data = createQuery({ - groups: { - name: 1, - authors: { - $filters: { - $meta: {isAdmin: true}, - }, - $options: {limit: 1}, - name: 1 - } - } - }).fetch(); - - let assertions = 0; - - _.each(data, group => { - _.each(group.authors, author => { - assert.isObject(author.$metadata); - assert.isTrue(author.$metadata.isAdmin); - assertions++; - }) - }); - - data = createQuery({ - groups: { - name: 1, - authors: { - $filters: { - $meta: {isAdmin: false}, - }, - $options: {limit: 1}, - name: 1 - } - } - }).fetch(); - - _.each(data, group => { - _.each(group.authors, author => { - assert.isObject(author.$metadata); - assert.isFalse(author.$metadata.isAdmin); - assertions++; - }) - }); - - assert.isTrue(assertions > 0); + data = await createQuery({ + groups: { + name: 1, + authors: { + $filters: { + $meta: { isAdmin: false }, + }, + $options: { limit: 1 }, + name: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (group) => { + _.each(group.authors, (author) => { + assert.isObject(author.$metadata); + assert.isFalse(author.$metadata.isAdmin); + assertions++; + }); }); + + assert.isTrue(assertions > 0); + }); }); diff --git a/lib/query/testing/reducers.client.test.js b/lib/query/testing/reducers.client.test.js index 8d369821..20ca289f 100755 --- a/lib/query/testing/reducers.client.test.js +++ b/lib/query/testing/reducers.client.test.js @@ -2,264 +2,263 @@ import { assert } from 'chai'; import { createQuery } from 'meteor/cultofcoders:grapher'; import waitForHandleToBeReady from './lib/waitForHandleToBeReady'; -describe('Client-side reducers', function() { - it('Should work with field only reducers', async function() { - const query = createQuery({ - authors: { - fullName: 1, - }, - }); - - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isString(author.fullName); - assert.isUndefined(author.name); - assert.isTrue(author.fullName.substr(0, 7) === 'full - '); - }); - - handle.stop(); +describe('Client-side reducers', function () { + it('Should work with field only reducers', async function () { + const query = createQuery({ + authors: { + fullName: 1, + }, }); - it('Should work with field only reducers and parameters', async function() { - const query = createQuery({ - authors: { - fullName: 1, - }, - }); + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); - query.setParams({ - suffix: 'Bomb', - }); + assert.isTrue(data.length > 0); - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); + data.forEach((author) => { + assert.isString(author.fullName); + assert.isUndefined(author.name); + assert.isTrue(author.fullName.substr(0, 7) === 'full - '); + }); - assert.isTrue(data.length > 0); + handle.stop(); + }); - data.forEach(author => { - assert.isString(author.fullName); - assert.isUndefined(author.name); - assert.isTrue(author.fullName.indexOf('Bomb') >= 0); - }); + it('Should work with field only reducers and parameters', async function () { + const query = createQuery({ + authors: { + fullName: 1, + }, + }); - handle.stop(); + query.setParams({ + suffix: 'Bomb', }); - it('Should work with nested fields reducers', async function() { - const query = createQuery({ - authors: { - fullNameNested: 1, - }, - }); + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isString(author.fullName); + assert.isUndefined(author.name); + assert.isTrue(author.fullName.indexOf('Bomb') >= 0); + }); + + handle.stop(); + }); + + it('Should work with nested fields reducers', async function () { + const query = createQuery({ + authors: { + fullNameNested: 1, + }, + }); - assert.isTrue(data.length > 0); + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); - data.forEach(author => { - assert.isString(author.fullNameNested); - assert.isString(author.fullNameNested); - assert.isFalse(author.fullNameNested === 'undefined undefined'); - assert.isUndefined(author.profile); - }); + assert.isTrue(data.length > 0); - handle.stop(); + data.forEach((author) => { + assert.isString(author.fullNameNested); + assert.isString(author.fullNameNested); + assert.isFalse(author.fullNameNested === 'undefined undefined'); + assert.isUndefined(author.profile); }); - it('Should work with nested fields reducers', async function() { - const query = createQuery({ - authors: { - profile: { - firstName: 1, - }, - fullNameNested: 1, - }, - }); + handle.stop(); + }); + + it('Should work with nested fields reducers', async function () { + const query = createQuery({ + authors: { + profile: { + firstName: 1, + }, + fullNameNested: 1, + }, + }); + + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isString(author.fullNameNested); + assert.isFalse(author.fullNameNested === 'undefined undefined'); + + assert.isObject(author.profile); + assert.isString(author.profile.firstName); + assert.isUndefined(author.profile.lastName); + }); - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); + handle.stop(); + }); - assert.isTrue(data.length > 0); + it('Should work with links reducers', async function () { + const query = createQuery({ + authors: { + groupNames: 1, + }, + }); - data.forEach(author => { - assert.isString(author.fullNameNested); - assert.isFalse(author.fullNameNested === 'undefined undefined'); + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); - assert.isObject(author.profile); - assert.isString(author.profile.firstName); - assert.isUndefined(author.profile.lastName); - }); + assert.isTrue(data.length > 0); - handle.stop(); + data.forEach((author) => { + assert.isArray(author.groupNames); + assert.isUndefined(author.groups); }); - it('Should work with links reducers', async function() { - const query = createQuery({ - authors: { - groupNames: 1, - }, - }); + handle.stop(); + }); - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); + it('Should work with links and nested reducers', async function () { + const query = createQuery({ + authors: { + referenceReducer: 1, + }, + }); - assert.isTrue(data.length > 0); + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); - data.forEach(author => { - assert.isArray(author.groupNames); - assert.isUndefined(author.groups); - }); + assert.isTrue(data.length > 0); - handle.stop(); + data.forEach((author) => { + assert.isString(author.referenceReducer); + assert.isUndefined(author.fullName); + assert.isTrue(author.referenceReducer.substr(0, 9) === 'nested - '); }); - it('Should work with links and nested reducers', async function() { - const query = createQuery({ - authors: { - referenceReducer: 1, - }, - }); + handle.stop(); + }); - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); + it('Should not clean nested reducers if not specified', async function () { + const query = createQuery({ + authors: { + referenceReducer: 1, + fullName: 1, + }, + }); - assert.isTrue(data.length > 0); + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); - data.forEach(author => { - assert.isString(author.referenceReducer); - assert.isUndefined(author.fullName); - assert.isTrue(author.referenceReducer.substr(0, 9) === 'nested - '); - }); + assert.isTrue(data.length > 0); - handle.stop(); + data.forEach((author) => { + assert.isString(author.referenceReducer); + assert.isString(author.fullName); }); - it('Should not clean nested reducers if not specified', async function() { - const query = createQuery({ - authors: { - referenceReducer: 1, - fullName: 1, - }, - }); + handle.stop(); + }); + + it('Should keep previously used items - Part 1', async function () { + const query = createQuery({ + authors: { + fullName: 1, + name: 1, + groupNames: 1, + groups: { + name: 1, + }, + }, + }); - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); - assert.isTrue(data.length > 0); + assert.isTrue(data.length > 0); - data.forEach(author => { - assert.isString(author.referenceReducer); - assert.isString(author.fullName); - }); + data.forEach((author) => { + assert.isDefined(author.name); + assert.isDefined(author.groups); + assert.isArray(author.groupNames); + assert.isString(author.fullName); + assert.isTrue(author.fullName.substr(0, 7) === 'full - '); + }); - handle.stop(); + handle.stop(); + }); + + it('Should keep previously used items - Part 2', async function () { + const query = createQuery({ + authors: { + groupNames: 1, + groups: { + _id: 1, + name: 1, + }, + }, }); - it('Should keep previously used items - Part 1', async function() { - const query = createQuery({ - authors: { - fullName: 1, - name: 1, - groupNames: 1, - groups: { - name: 1, - }, - }, - }); - - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isDefined(author.name); - assert.isDefined(author.groups); - assert.isArray(author.groupNames); - assert.isString(author.fullName); - assert.isTrue(author.fullName.substr(0, 7) === 'full - '); - }); - - handle.stop(); + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isDefined(author.groups); + assert.isArray(author.groupNames); + + author.groupNames.forEach((groupName) => { + assert.isTrue(groupName.length > 2); + assert.isTrue(groupName.substr(0, 2) == 'G#'); + assert.isFalse(groupName.slice(2) === 'undefined'); + }); + + author.groups.forEach((group) => { + assert.isDefined(group._id); + assert.isDefined(group.name); + }); }); - it('Should keep previously used items - Part 2', async function() { - const query = createQuery({ - authors: { - groupNames: 1, - groups: { - _id: 1, - name: 1, - }, - }, - }); - - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isDefined(author.groups); - assert.isArray(author.groupNames); - - author.groupNames.forEach(groupName => { - assert.isTrue(groupName.length > 2); - assert.isTrue(groupName.substr(0, 2) == 'G#'); - assert.isFalse(groupName.slice(2) === 'undefined'); - }); - - author.groups.forEach(group => { - assert.isDefined(group._id); - assert.isDefined(group.name); - }); - }); - - handle.stop(); + handle.stop(); + }); + + it('Should work with denormalized fields', async function () { + const query = createQuery({ + groups: { + posts: { + authorCached: { + name: 1, + }, + }, + }, }); - it('Should work with denormalized fields', async function() { - const query = createQuery({ - groups: { - posts: { - authorCached: { - name: 1, - } - }, - } - }); - - let handle = query.subscribe(); - await waitForHandleToBeReady(handle); - const data = query.fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(data => { - data.posts.forEach(post => { - assert.isObject(post.authorCached); - assert.isDefined(post.authorCached.name); - - - // denormalized field should not be present - assert.isUndefined(post.authorCache); - }); - }); - - handle.stop(); + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); + + assert.isTrue(data.length > 0); + + data.forEach((data) => { + data.posts.forEach((post) => { + assert.isObject(post.authorCached); + assert.isDefined(post.authorCached.name); + + // denormalized field should not be present + assert.isUndefined(post.authorCache); + }); }); + + handle.stop(); + }); }); diff --git a/lib/query/testing/reducers.server.test.js b/lib/query/testing/reducers.server.test.js index 419c78ab..9b3b6b6a 100755 --- a/lib/query/testing/reducers.server.test.js +++ b/lib/query/testing/reducers.server.test.js @@ -3,392 +3,390 @@ import { createQuery } from 'meteor/cultofcoders:grapher'; import Authors from './bootstrap/authors/collection'; import Comments from './bootstrap/comments/collection'; -describe('Reducers', function() { - it('Should work with field only reducers', function() { - const data = createQuery({ - authors: { - fullName: 1, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isString(author.fullName); - assert.isUndefined(author.name); - assert.isTrue(author.fullName.substr(0, 7) === 'full - '); - }); +describe('Reducers', function () { + it('Should work with field only reducers', async function () { + const data = await createQuery({ + authors: { + fullName: 1, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isString(author.fullName); + assert.isUndefined(author.name); + assert.isTrue(author.fullName.substr(0, 7) === 'full - '); }); - - it('Should work with nested fields reducers', function() { - const data = createQuery({ - authors: { - fullNameNested: 1, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isString(author.fullNameNested); - assert.isString(author.fullNameNested); - assert.isFalse(author.fullNameNested === 'undefined undefined'); - assert.isUndefined(author.profile); - }); + }); + + it('Should work with nested fields reducers', async function () { + const data = await createQuery({ + authors: { + fullNameNested: 1, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isString(author.fullNameNested); + assert.isString(author.fullNameNested); + assert.isFalse(author.fullNameNested === 'undefined undefined'); + assert.isUndefined(author.profile); }); - - it('Should work with nested fields reducers - 2', function() { - const data = createQuery({ - authors: { - // reducer with {profile: {firstName: 1, lastName: 1}} - fullNameNested: 1, - profile: 1, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isString(author.fullNameNested); - assert.isString(author.fullNameNested); - assert.isFalse(author.fullNameNested === 'undefined undefined'); - assert.isObject(author.profile); - assert.isString(author.profile.firstName); - assert.isString(author.profile.lastName); - }); + }); + + it('Should work with nested fields reducers - 2', async function () { + const data = await createQuery({ + authors: { + // reducer with {profile: {firstName: 1, lastName: 1}} + fullNameNested: 1, + profile: 1, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isString(author.fullNameNested); + assert.isString(author.fullNameNested); + assert.isFalse(author.fullNameNested === 'undefined undefined'); + assert.isObject(author.profile); + assert.isString(author.profile.firstName); + assert.isString(author.profile.lastName); }); - - it('Should work with nested fields reducers - 3', function() { - const data = createQuery({ - authors: { - // reducer with {profile: 1} - fullNameNested2: 1, - profile: { - firstName: 1 - }, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isString(author.fullNameNested2); - assert.isObject(author.profile); - assert.isString(author.profile.firstName); - // TODO: cleaning should be updated for this to work, currently this field is regular string - // assert.isUndefined(author.profile.lastName); - }); + }); + + it('Should work with nested fields reducers - 3', async function () { + const data = await createQuery({ + authors: { + // reducer with {profile: 1} + fullNameNested2: 1, + profile: { + firstName: 1, + }, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isString(author.fullNameNested2); + assert.isObject(author.profile); + assert.isString(author.profile.firstName); + // TODO: cleaning should be updated for this to work, currently this field is regular string + // assert.isUndefined(author.profile.lastName); }); - - it('Should work with deep reducers', function() { - const data = createQuery({ - posts: { - $options: { limit: 5 }, - author: { - fullName: 1, - fullNameNested: 1, - }, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(post => { - const author = post.author; - assert.isUndefined(author.name); - assert.isTrue(author.fullName.substr(0, 7) === 'full - '); - }); + }); + + it('Should work with deep reducers', async function () { + const data = await createQuery({ + posts: { + $options: { limit: 5 }, + author: { + fullName: 1, + fullNameNested: 1, + }, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((post) => { + const author = post.author; + assert.isUndefined(author.name); + assert.isTrue(author.fullName.substr(0, 7) === 'full - '); }); - - it('Should work with nested fields reducers', function() { - const data = createQuery({ - authors: { - profile: { - firstName: 1, - }, - fullNameNested: 1, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isString(author.fullNameNested); - assert.isFalse(author.fullNameNested === 'undefined undefined'); - - assert.isObject(author.profile); - assert.isString(author.profile.firstName); - assert.isUndefined(author.profile.lastName); - }); + }); + + it('Should work with nested fields reducers', async function () { + const data = await createQuery({ + authors: { + profile: { + firstName: 1, + }, + fullNameNested: 1, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isString(author.fullNameNested); + assert.isFalse(author.fullNameNested === 'undefined undefined'); + + assert.isObject(author.profile); + assert.isString(author.profile.firstName); + assert.isUndefined(author.profile.lastName); }); + }); - it('Should work with links reducers', function() { - const data = createQuery({ - authors: { - groupNames: 1, - }, - }).fetch(); + it('Should work with links reducers', async function () { + const data = await createQuery({ + authors: { + groupNames: 1, + }, + }).fetchAsync(); - assert.isTrue(data.length > 0); + assert.isTrue(data.length > 0); - data.forEach(author => { - assert.isArray(author.groupNames); - assert.isUndefined(author.groups); - }); + data.forEach((author) => { + assert.isArray(author.groupNames); + assert.isUndefined(author.groups); }); - - it('Should work with One link reducers', function() { - const sampleComment = Comments.findOne(); - - const comment = createQuery({ - comments: { - $filters: { - _id: sampleComment._id, - }, - authorLinkReducer: 1, - }, - }).fetchOne(); - - assert.isObject(comment); - assert.isObject(comment.authorLinkReducer); + }); + + it('Should work with One link reducers', async function () { + const sampleComment = await Comments.findOneAsync(); + + const comment = await createQuery({ + comments: { + $filters: { + _id: sampleComment._id, + }, + authorLinkReducer: 1, + }, + }).fetchOneAsync(); + + assert.isObject(comment); + assert.isObject(comment.authorLinkReducer); + }); + + it('Should work with links and nested reducers', async function () { + const data = await createQuery({ + authors: { + referenceReducer: 1, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isString(author.referenceReducer); + assert.isUndefined(author.fullName); + assert.isTrue(author.referenceReducer.substr(0, 9) === 'nested - '); }); + }); - it('Should work with links and nested reducers', function() { - const data = createQuery({ - authors: { - referenceReducer: 1, - }, - }).fetch(); + it('Should not clean nested reducers if not specified', async function () { + const data = await createQuery({ + authors: { + referenceReducer: 1, + }, + }).fetchAsync(); - assert.isTrue(data.length > 0); + assert.isTrue(data.length > 0); - data.forEach(author => { - assert.isString(author.referenceReducer); - assert.isUndefined(author.fullName); - assert.isTrue(author.referenceReducer.substr(0, 9) === 'nested - '); - }); + data.forEach((author) => { + assert.isString(author.referenceReducer); + assert.isUndefined(author.fullName); + assert.isTrue(author.referenceReducer.substr(0, 9) === 'nested - '); }); + }); - it('Should not clean nested reducers if not specified', function() { - const data = createQuery({ - authors: { - referenceReducer: 1, - }, - }).fetch(); + it('Should not clean nested reducers if not specified', async function () { + const data = await createQuery({ + authors: { + referenceReducer: 1, + fullName: 1, + }, + }).fetchAsync(); - assert.isTrue(data.length > 0); + assert.isTrue(data.length > 0); - data.forEach(author => { - assert.isString(author.referenceReducer); - assert.isUndefined(author.fullName); - assert.isTrue(author.referenceReducer.substr(0, 9) === 'nested - '); - }); + data.forEach((author) => { + assert.isString(author.referenceReducer); + assert.isString(author.fullName); }); - - it('Should not clean nested reducers if not specified', function() { - const data = createQuery({ - authors: { - referenceReducer: 1, - fullName: 1, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isString(author.referenceReducer); - assert.isString(author.fullName); - }); + }); + + it('Should keep previously used items - Part 1', async function () { + const data = await createQuery({ + authors: { + fullName: 1, + name: 1, + groupNames: 1, + groups: { + name: 1, + }, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isDefined(author.name); + assert.isDefined(author.groups); + assert.isArray(author.groupNames); + assert.isString(author.fullName); + assert.isTrue(author.fullName.substr(0, 7) === 'full - '); }); - - it('Should keep previously used items - Part 1', function() { - const data = createQuery({ - authors: { - fullName: 1, - name: 1, - groupNames: 1, - groups: { - name: 1, - }, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isDefined(author.name); - assert.isDefined(author.groups); - assert.isArray(author.groupNames); - assert.isString(author.fullName); - assert.isTrue(author.fullName.substr(0, 7) === 'full - '); - }); + }); + + it('Should work with deep reducers', async function () { + const data = await createQuery({ + posts: { + $options: { limit: 5 }, + author: { + fullName: 1, + fullNameNested: 1, + }, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((post) => { + const author = post.author; + assert.isUndefined(author.name); + assert.isTrue(author.fullName.substr(0, 7) === 'full - '); }); - - it('Should work with deep reducers', function() { - const data = createQuery({ - posts: { - $options: { limit: 5 }, - author: { - fullName: 1, - fullNameNested: 1, - }, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(post => { - const author = post.author; - assert.isUndefined(author.name); - assert.isTrue(author.fullName.substr(0, 7) === 'full - '); - }); + }); + + it('Should keep previously used items - Part 2', async function () { + const data = await createQuery({ + authors: { + groupNames: 1, + groups: { + _id: 1, + }, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isDefined(author.groups); + assert.isArray(author.groupNames); + + author.groupNames.forEach((groupName) => { + assert.isTrue(groupName.length > 2); + assert.isTrue(groupName.substr(0, 2) == 'G#'); + assert.isFalse(groupName.slice(2) === 'undefined'); + }); + + author.groups.forEach((group) => { + assert.isDefined(group._id); + assert.isUndefined(group.name); + }); }); - - it('Should keep previously used items - Part 2', function() { - const data = createQuery({ - authors: { - groupNames: 1, - groups: { - _id: 1, - }, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isDefined(author.groups); - assert.isArray(author.groupNames); - - author.groupNames.forEach(groupName => { - assert.isTrue(groupName.length > 2); - assert.isTrue(groupName.substr(0, 2) == 'G#'); - assert.isFalse(groupName.slice(2) === 'undefined'); - }); - - author.groups.forEach(group => { - assert.isDefined(group._id); - assert.isUndefined(group.name); - }); - }); + }); + + it('Should work with params reducers', async function () { + const query = createQuery({ + authors: { + $options: { limit: 1 }, + paramBasedReducer: 1, + }, }); - it('Should work with params reducers', function() { - const query = createQuery({ - authors: { - $options: { limit: 1 }, - paramBasedReducer: 1, - }, - }); - - query.setParams({ - element: 'TEST_STRING', - }); + query.setParams({ + element: 'TEST_STRING', + }); - const data = query.fetch(); + const data = await query.fetchAsync(); - assert.isTrue(data.length > 0); - data.forEach(author => { - assert.equal(author.paramBasedReducer, 'TEST_STRING'); - }); + assert.isTrue(data.length > 0); + data.forEach((author) => { + assert.equal(author.paramBasedReducer, 'TEST_STRING'); }); - - it('Should work with reducers that use deep denormalized nested fields', function() { - /** - * Both commentsReducers use Posts link on Authors collection and both use denormalized authorCached link - * inside the Posts. - * - * This necessitates the use of embedReducerWithLink() function while creating reducers, - * which was failing for denormalized fields and also for nested fields. - * - * Also, the commentsReducer2 uses nested item in the body, profile: {lastName: 1} - */ - const query = createQuery({ - authors: { - commentsReducer1: 1, - commentsReducer2: 1, - }, - }); - - const data = query.fetch(); - - assert.isTrue(data.length > 0); - data.forEach(author => { - // check if nested denormalized links are working - assert.isObject(author.commentsReducer2.author); - assert.isTrue(author.commentsReducer2.author.name.startsWith('Author')); - - // check if nested fields are working - assert.isObject(author.commentsReducer2.metadata); - assert.isTrue(author.commentsReducer2.metadata.keywords.length > 0); - }); + }); + + it('Should work with reducers that use deep denormalized nested fields', async function () { + /** + * Both commentsReducers use Posts link on Authors collection and both use denormalized authorCached link + * inside the Posts. + * + * This necessitates the use of embedReducerWithLink() function while creating reducers, + * which was failing for denormalized fields and also for nested fields. + * + * Also, the commentsReducer2 uses nested item in the body, profile: {lastName: 1} + */ + const query = createQuery({ + authors: { + commentsReducer1: 1, + commentsReducer2: 1, + }, }); - it('Should allow non-existent nested fields while cleaning', function() { - const query = createQuery({ - posts: { - reducerNonExistentNestedField: 1, - }, - }); + const data = await query.fetchAsync(); + + assert.isTrue(data.length > 0); + data.forEach((author) => { + // check if nested denormalized links are working + assert.isObject(author.commentsReducer2.author); + assert.isTrue(author.commentsReducer2.author.name.startsWith('Author')); - const data = query.fetch({limit: 1}); - assert.equal(data[0].reducerNonExistentNestedField, 'null'); + // check if nested fields are working + assert.isObject(author.commentsReducer2.metadata); + assert.isTrue(author.commentsReducer2.metadata.keywords.length > 0); }); + }); - it('Should work with reducer expanders', function() { - const data = createQuery({ - authors: { - expandNameAndGroups: 1, - }, - }).fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - assert.isUndefined(author.expandNameAndGroups); - assert.isString(author.name); - assert.isArray(author.groups); - - author.groups.forEach(group => { - assert.isDefined(group._id); - assert.isString(group.name); - }); - }); + it('Should allow non-existent nested fields while cleaning', async function () { + const query = createQuery({ + posts: { + reducerNonExistentNestedField: 1, + }, }); + const data = await query.fetchAsync({ limit: 1 }); + assert.equal(data[0].reducerNonExistentNestedField, 'null'); + }); - it('Should work with reducer expanders and nested fields + graph-like query', function() { - const data = createQuery({ - authors: { - 'profile': { - test: 1, - }, - }, - }).fetch(); + it('Should work with reducer expanders', async function () { + const data = await createQuery({ + authors: { + expandNameAndGroups: 1, + }, + }).fetchAsync(); - assert.isTrue(data.length > 0); + assert.isTrue(data.length > 0); - data.forEach(author => { - assert.isUndefined(author.profile.test); - assert.isString(author.profile.firstName); - assert.isUndefined(author.profile.lastName); - }); - }); + data.forEach((author) => { + assert.isUndefined(author.expandNameAndGroups); + assert.isString(author.name); + assert.isArray(author.groups); + author.groups.forEach((group) => { + assert.isDefined(group._id); + assert.isString(group.name); + }); + }); + }); + + it('Should work with reducer expanders and nested fields + graph-like query', async function () { + const data = await createQuery({ + authors: { + profile: { + test: 1, + }, + }, + }).fetchAsync(); + + assert.isTrue(data.length > 0); + + data.forEach((author) => { + assert.isUndefined(author.profile.test); + assert.isString(author.profile.firstName); + assert.isUndefined(author.profile.lastName); + }); + }); - it('Should work with reducer expanders and nested fields + nested-like query', function() { - const data = createQuery({ - authors: { - 'profile.test': 1, - }, - }).fetch(); + it('Should work with reducer expanders and nested fields + nested-like query', async function () { + const data = await createQuery({ + authors: { + 'profile.test': 1, + }, + }).fetchAsync(); - assert.isTrue(data.length > 0); + assert.isTrue(data.length > 0); - data.forEach(author => { - assert.isUndefined(author.profile.test); - assert.isString(author.profile.firstName); - assert.isUndefined(author.profile.lastName); - }); + data.forEach((author) => { + assert.isUndefined(author.profile.test); + assert.isString(author.profile.firstName); + assert.isUndefined(author.profile.lastName); }); + }); }); diff --git a/lib/query/testing/security.client.test.js b/lib/query/testing/security.client.test.js index 2b7c97dc..f81f550f 100755 --- a/lib/query/testing/security.client.test.js +++ b/lib/query/testing/security.client.test.js @@ -3,23 +3,23 @@ import { createQuery } from 'meteor/cultofcoders:grapher'; import waitForHandleToBeReady from './lib/waitForHandleToBeReady'; describe('Query Security Client Tests', function () { - it('Should not retrieve subitems with reactive and non-reactive query', async function () { - const query = createQuery({ - security_items: { - text: 1, - subitems: { - text: 1 - } - } - }); + it('Should not retrieve subitems with reactive and non-reactive query', async function () { + const query = createQuery({ + security_items: { + text: 1, + subitems: { + text: 1, + }, + }, + }); - const handle = query.subscribe(); + const handle = query.subscribe(); - await waitForHandleToBeReady(handle); + await waitForHandleToBeReady(handle); - const data = query.fetch(); + const data = query.fetch(); - assert.lengthOf(data, 1); - assert.lengthOf(data[0].subitems, 0); - }); -}); \ No newline at end of file + assert.lengthOf(data, 1); + assert.lengthOf(data[0].subitems, 0); + }); +}); diff --git a/lib/query/testing/server.test.js b/lib/query/testing/server.test.js index 5a6c3846..e0d19c9c 100755 --- a/lib/query/testing/server.test.js +++ b/lib/query/testing/server.test.js @@ -1,52 +1,52 @@ -import { assert, expect } from "chai"; -import dot from "dot-object"; - -import { createQuery } from "meteor/cultofcoders:grapher"; -import { Random } from "meteor/random"; -import Comments from "./bootstrap/comments/collection.js"; -import Posts from "./bootstrap/posts/collection.js"; -import Tags from "./bootstrap/tags/collection.js"; -import { Files } from "./bootstrap/files/collection"; -import { Projects } from "./bootstrap/projects/collection"; -import "./metaFilters.server.test"; -import "./reducers.server.test"; -import "./link-cache/server.test"; -import intersectDeep from "../lib/intersectDeep.js"; +import { assert, expect } from 'chai'; + +import { createQuery } from 'meteor/cultofcoders:grapher'; +import { Random } from 'meteor/random'; +import Comments from './bootstrap/comments/collection.js'; +import Posts from './bootstrap/posts/collection.js'; +import Tags from './bootstrap/tags/collection.js'; +import { Files } from './bootstrap/files/collection'; +import { Projects } from './bootstrap/projects/collection'; +import './metaFilters.server.test'; +import './reducers.server.test'; +import './link-cache/server.test'; +import intersectDeep from '../lib/intersectDeep.js'; +import { _ } from 'meteor/underscore'; // Used in some tests below -const Users = new Mongo.Collection("__many_inversed_users"); -const Restaurants = new Mongo.Collection("__many_inversed_restaurants"); -const ShoppingCart = new Mongo.Collection("__projection_operators_cart"); -const Clients = new Mongo.Collection("__text_search_clients"); -Clients._ensureIndex({ name: "text" }); +const Users = new Mongo.Collection('__many_inversed_users'); +const Restaurants = new Mongo.Collection('__many_inversed_restaurants'); +const ShoppingCart = new Mongo.Collection('__projection_operators_cart'); +const Clients = new Mongo.Collection('__text_search_clients'); +await Clients.createIndexAsync({ name: 'text' }); Clients.addLinks({ - shoppingCart: { - type: "one", - collection: ShoppingCart, - metadata: true, - field: "shoppingCartData", - unique: true - }, - - shoppingCarts: { - collection: ShoppingCart, - type: "many", - metadata: true, - field: "shoppingCartsData" - } + shoppingCart: { + type: 'one', + collection: ShoppingCart, + metadata: true, + field: 'shoppingCartData', + unique: true, + }, + + shoppingCarts: { + collection: ShoppingCart, + type: 'many', + metadata: true, + field: 'shoppingCartsData', + }, }); ShoppingCart.addLinks({ - user: { - collection: Clients, - inversedBy: "shoppingCart" - }, - - users: { - collection: Clients, - inversedBy: "shoppingCarts" - } + user: { + collection: Clients, + inversedBy: 'shoppingCart', + }, + + users: { + collection: Clients, + inversedBy: 'shoppingCarts', + }, }); // for storeOneResults tests @@ -55,1672 +55,1683 @@ const Level2 = new Mongo.Collection('level2'); const Level3 = new Mongo.Collection('level3'); const Level4 = new Mongo.Collection('level4'); Level1.addLinks({ - level2: { - type: 'one', - collection: Level2, - field: 'level2Id', - }, + level2: { + type: 'one', + collection: Level2, + field: 'level2Id', + }, }); Level2.addLinks({ - level3: { - type: 'one', - collection: Level3, - field: 'level3Id', - }, + level3: { + type: 'one', + collection: Level3, + field: 'level3Id', + }, }); Level3.addLinks({ - level4: { - type: 'one', - collection: Level4, - field: 'level4Id', - }, -}) - -describe("Hypernova", function() { - - it("Should not crash due to nested filters", () => { - const id = `Nested filters_${Random.id()}`; - const A = new Mongo.Collection(`${id}_a`); - const B = new Mongo.Collection(`${id}_b`); - const C = new Mongo.Collection(`${id}_c`); - - B.addLinks({ - as: { - type: 'many', - collection: A, - field: 'a_ids', - unique: true, - } - }); + level4: { + type: 'one', + collection: Level4, + field: 'level4Id', + }, +}); - A.addLinks({ - b: { - collection: B, - inversedBy: 'as', - }, +describe('Hypernova', function () { + it('Should not crash due to nested filters', async () => { + const id = `Nested filters_${Random.id()}`; + const A = new Mongo.Collection(`${id}_a`); + const B = new Mongo.Collection(`${id}_b`); + const C = new Mongo.Collection(`${id}_c`); + + B.addLinks({ + as: { + type: 'many', + collection: A, + field: 'a_ids', + unique: true, + }, + }); - c: { - type: 'one', - collection: C, - field: 'c_id', - }, - }); + A.addLinks({ + b: { + collection: B, + inversedBy: 'as', + }, - const cId = C.insert({ - foo: true - }); + c: { + type: 'one', + collection: C, + field: 'c_id', + }, + }); - const aIdA = A.insert({ - foo: true, - bar: true, - c_id: cId, - }); + const cId = await C.insertAsync({ + foo: true, + }); - const aIdB = A.insert({ - foo: true, - bar: false, - c_id: cId, - }); + const aIdA = await A.insertAsync({ + foo: true, + bar: true, + c_id: cId, + }); - B.insert({ - foo: true, - a_ids: [aIdA, aIdB], - }); + const aIdB = await A.insertAsync({ + foo: true, + bar: false, + c_id: cId, + }); - const data = A.createQuery({ + await B.insertAsync({ + foo: true, + a_ids: [aIdA, aIdB], + }); + + const data = await A.createQuery({ + $filters: { + foo: true, + }, + b: { + $filters: { + foo: true, + }, + as: { $filters: { foo: true, + bar: true, }, - b: { - $filters: { - foo: true, - }, - as: { - $filters: { - foo: true, - bar: true, - }, - c: { - foo: 1, - } - } - } - }).fetch(); - - assert.lengthOf(data, 2); - assert.lengthOf(data[0].b.as, 1); - assert.lengthOf(data[1].b.as, 1); - assert.isTrue(data[0].b.as[0].c.foo); - assert.isTrue(data[1].b.as[0].c.foo); - }); - - it("Should support projection operators", () => { - ShoppingCart.remove({}); - ShoppingCart.insert({ - date: new Date(), - items: [ - { - title: "Item 1", - price: 30 - }, - { - title: "Item 2", - price: 50 - } - ] - }); - - const data = ShoppingCart.createQuery({ - items: { $elemMatch: { price: { $gt: 40 } } } - }).fetch(); - - assert.lengthOf(data, 1); - assert.lengthOf(data[0].items, 1); + c: { + foo: 1, + }, + }, + }, + }).fetchAsync(); + + assert.lengthOf(data, 2); + assert.lengthOf(data[0].b.as, 1); + assert.lengthOf(data[1].b.as, 1); + assert.isTrue(data[0].b.as[0].c.foo); + assert.isTrue(data[1].b.as[0].c.foo); + }); + + it('Should support projection operators', async () => { + await ShoppingCart.removeAsync({}); + await ShoppingCart.insertAsync({ + date: new Date(), + items: [ + { + title: 'Item 1', + price: 30, + }, + { + title: 'Item 2', + price: 50, + }, + ], }); - it("Should properly handle text search with sorting and score value projection", () => { - Clients.remove({}); - Clients.insert({ name: "John Doe", age: 23 }); - Clients.insert({ name: "John F McNull", age: 23 }); - Clients.insert({ name: "Mary Smith", age: 40 }); - - const data = Clients.createQuery({ - $filters: { - $text: { $search: "john" } - }, - $options: { - sort: { - score: { $meta: "textScore" } - } - }, - score: { $meta: "textScore" } - }).fetch(); - - assert.lengthOf(data, 2); - data.forEach(client => { - // unspecified fields must be excluded - assert.isUndefined(client.name); - assert.isUndefined(client.age); - - // _id and score should be included - assert.isString(client._id); - assert.isNumber(client.score); - }); - - // sort check - const [client1, client2] = data; - assert.isTrue(client1.score > client2.score); - }); - - it("Should fetch One links correctly", function() { - const data = createQuery({ - comments: { - text: 1, - author: { - name: 1 - } - } - }).fetch(); - - assert.lengthOf(data, Comments.find().count()); - assert.isTrue(data.length > 0); - - _.each(data, comment => { - assert.isObject(comment.author); - assert.isString(comment.author.name); - assert.isString(comment.author._id); - assert.isTrue(_.keys(comment.author).length == 2); - }); + const data = await ShoppingCart.createQuery({ + items: { $elemMatch: { price: { $gt: 40 } } }, + }).fetchAsync(); + + assert.lengthOf(data, 1); + assert.lengthOf(data[0].items, 1); + }); + + it('Should properly handle text search with sorting and score value projection', async () => { + await Clients.removeAsync({}); + await Clients.insertAsync({ name: 'John Doe', age: 23 }); + await Clients.insertAsync({ name: 'John F McNull', age: 23 }); + await Clients.insertAsync({ name: 'Mary Smith', age: 40 }); + + const data = await Clients.createQuery({ + $filters: { + $text: { $search: 'john' }, + }, + $options: { + sort: { + score: { $meta: 'textScore' }, + }, + }, + score: { $meta: 'textScore' }, + }).fetchAsync(); + + assert.lengthOf(data, 2); + data.forEach((client) => { + // unspecified fields must be excluded + assert.isUndefined(client.name); + assert.isUndefined(client.age); + + // _id and score should be included + assert.isString(client._id); + assert.isNumber(client.score); }); - it("Should fetch One links with limit and options", function() { - const data = createQuery({ - comments: { - $options: { limit: 5 }, - text: 1 - } - }).fetch(); - - assert.lengthOf(data, 5); - }); - - it("Should fetch One-Inversed links with limit and options", function() { - const query = createQuery( - { - authors: { - $options: { limit: 5 }, - comments: { - $filters: { text: "Good" }, - $options: { limit: 2 }, - text: 1 - } - } - }, - {}, - { debug: true } - ); - - const data = query.fetch(); - - assert.lengthOf(data, 5); - _.each(data, author => { - assert.lengthOf(author.comments, 2); - _.each(author.comments, comment => { - assert.equal("Good", comment.text); - }); - }); + // sort check + const [client1, client2] = data; + assert.isTrue(client1.score > client2.score); + }); + + it('Should fetch One links correctly', async function () { + const data = await createQuery({ + comments: { + text: 1, + author: { + name: 1, + }, + }, + }).fetchAsync(); + + assert.lengthOf(data, await Comments.find().countAsync()); + assert.isTrue(data.length > 0); + + _.each(data, (comment) => { + assert.isObject(comment.author); + assert.isString(comment.author.name); + assert.isString(comment.author._id); + assert.isTrue(_.keys(comment.author).length == 2); }); - - it("Should fetch Many links correctly", function() { - const data = createQuery({ - posts: { - $options: { limit: 5 }, - title: 1, - tags: { - text: 1 - } - } - }).fetch(); - - assert.lengthOf(data, 5); - _.each(data, post => { - assert.isString(post.title); - assert.isArray(post.tags); - assert.isTrue(post.tags.length > 0); - }); + }); + + it('Should fetch One links with limit and options', async function () { + const data = await createQuery({ + comments: { + $options: { limit: 5 }, + text: 1, + }, + }).fetchAsync(); + + assert.lengthOf(data, 5); + }); + + it('Should fetch One-Inversed links with limit and options', async function () { + const query = createQuery( + { + authors: { + $options: { limit: 5 }, + comments: { + $filters: { text: 'Good' }, + $options: { limit: 2 }, + text: 1, + }, + }, + }, + {}, + { debug: true }, + ); + + const data = await query.fetchAsync(); + + assert.lengthOf(data, 5); + _.each(data, (author) => { + assert.lengthOf(author.comments, 2); + _.each(author.comments, (comment) => { + assert.equal('Good', comment.text); + }); }); - - it("Should fetch Many - inversed links correctly", function() { - const data = createQuery({ - tags: { - name: 1, - posts: { - $options: { limit: 5 }, - title: 1 - } - } - }).fetch(); - - _.each(data, tag => { - assert.isString(tag.name); - assert.isArray(tag.posts); - assert.isTrue(tag.posts.length <= 5); - _.each(tag.posts, post => { - assert.isString(post.title); - }); - }); + }); + + it('Should fetch Many links correctly', async function () { + const data = await createQuery({ + posts: { + $options: { limit: 5 }, + title: 1, + tags: { + text: 1, + }, + }, + }).fetchAsync(); + + assert.lengthOf(data, 5); + _.each(data, (post) => { + assert.isString(post.title); + assert.isArray(post.tags); + assert.isTrue(post.tags.length > 0); + }); + }); + + it('Should fetch Many - inversed links correctly', async function () { + const data = await createQuery({ + tags: { + name: 1, + posts: { + $options: { limit: 5 }, + title: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (tag) => { + assert.isString(tag.name); + assert.isArray(tag.posts); + assert.isTrue(tag.posts.length <= 5); + _.each(tag.posts, (post) => { + assert.isString(post.title); + }); }); + }); + + it('Should fetch Many - inversed links correctly #2', async function () { + const post1Id = await Posts.insertAsync({ name: 'Post1' }); + const post2Id = await Posts.insertAsync({ name: 'Post2' }); + const post3Id = await Posts.insertAsync({ name: 'Post3' }); + const post4Id = await Posts.insertAsync({ name: 'Post4' }); + + const tag1Id = await Tags.insertAsync({ name: 'Tag1' }); + const tag2Id = await Tags.insertAsync({ name: 'Tag2' }); + const tag3Id = await Tags.insertAsync({ name: 'Tag3' }); + + async function addTags(postId, tagIds) { + return Posts.updateAsync(postId, { + $set: { + tagIds, + }, + }); + } - it("Should fetch Many - inversed links correctly #2", function() { - const post1Id = Posts.insert({ name: "Post1" }); - const post2Id = Posts.insert({ name: "Post2" }); - const post3Id = Posts.insert({ name: "Post3" }); - const post4Id = Posts.insert({ name: "Post4" }); + await addTags(post1Id, [tag1Id, tag2Id]); + await addTags(post2Id, [tag1Id]); + await addTags(post3Id, [tag2Id, tag3Id]); + await addTags(post4Id, [tag3Id, tag1Id]); + + const data = await createQuery({ + tags: { + $filters: { + _id: { $in: [tag1Id, tag2Id, tag3Id] }, + }, + name: 1, + posts: { + name: 1, + }, + }, + }).fetchAsync(); + + const tag1Data = _.find(data, (doc) => doc.name === 'Tag1'); + const tag2Data = _.find(data, (doc) => doc.name === 'Tag2'); + const tag3Data = _.find(data, (doc) => doc.name === 'Tag3'); + + function hasPost(tag, postName) { + return !!_.find(tag.posts, (post) => post.name === postName); + } + assert.lengthOf(tag1Data.posts, 3); + assert.isTrue(hasPost(tag1Data, 'Post1')); + assert.isTrue(hasPost(tag1Data, 'Post2')); + assert.isTrue(hasPost(tag1Data, 'Post4')); - const tag1Id = Tags.insert({ name: "Tag1" }); - const tag2Id = Tags.insert({ name: "Tag2" }); - const tag3Id = Tags.insert({ name: "Tag3" }); + assert.lengthOf(tag2Data.posts, 2); + assert.isTrue(hasPost(tag2Data, 'Post1')); + assert.isTrue(hasPost(tag2Data, 'Post3')); - function addTags(postId, tagIds) { - Posts.update(postId, { - $set: { - tagIds - } - }); - } + assert.lengthOf(tag3Data.posts, 2); + assert.isTrue(hasPost(tag3Data, 'Post3')); + assert.isTrue(hasPost(tag3Data, 'Post4')); - addTags(post1Id, [tag1Id, tag2Id]); - addTags(post2Id, [tag1Id]); - addTags(post3Id, [tag2Id, tag3Id]); - addTags(post4Id, [tag3Id, tag1Id]); - - const data = createQuery({ - tags: { - $filters: { - _id: { $in: [tag1Id, tag2Id, tag3Id] } - }, - name: 1, - posts: { - name: 1 - } - } - }).fetch(); - - const tag1Data = _.find(data, doc => doc.name === "Tag1"); - const tag2Data = _.find(data, doc => doc.name === "Tag2"); - const tag3Data = _.find(data, doc => doc.name === "Tag3"); - - function hasPost(tag, postName) { - return !!_.find(tag.posts, post => post.name === postName); - } - assert.lengthOf(tag1Data.posts, 3); - assert.isTrue(hasPost(tag1Data, "Post1")); - assert.isTrue(hasPost(tag1Data, "Post2")); - assert.isTrue(hasPost(tag1Data, "Post4")); - - assert.lengthOf(tag2Data.posts, 2); - assert.isTrue(hasPost(tag2Data, "Post1")); - assert.isTrue(hasPost(tag2Data, "Post3")); - - assert.lengthOf(tag3Data.posts, 2); - assert.isTrue(hasPost(tag3Data, "Post3")); - assert.isTrue(hasPost(tag3Data, "Post4")); - - Posts.remove({ - _id: { $in: [post1Id, post2Id, post3Id, post4Id] } - }); - Tags.remove({ - _id: { $in: [tag1Id, tag2Id, tag3Id] } - }); + await Posts.removeAsync({ + _id: { $in: [post1Id, post2Id, post3Id, post4Id] }, }); - - it("Should fetch One-Meta links correctly", function() { - const data = createQuery({ - posts: { - $options: { limit: 5 }, - title: 1, - group: { - name: 1 - } - } - }).fetch(); - - assert.lengthOf(data, 5); - _.each(data, post => { - assert.isString(post.title); - assert.isString(post._id); - assert.isObject(post.group); - assert.isString(post.group._id); - assert.isString(post.group.name); - }); + await Tags.removeAsync({ + _id: { $in: [tag1Id, tag2Id, tag3Id] }, }); - - it("Should fetch One-Meta inversed links correctly", function() { - const data = createQuery({ - groups: { - name: 1, - posts: { - title: 1 - } - } - }).fetch(); - - _.each(data, group => { - assert.isString(group.name); - assert.isString(group._id); - assert.lengthOf(_.keys(group), 3); - assert.isArray(group.posts); - _.each(group.posts, post => { - assert.isString(post.title); - assert.isString(post._id); - }); - }); + }); + + it('Should fetch One-Meta links correctly', async function () { + const data = await createQuery({ + posts: { + $options: { limit: 5 }, + title: 1, + group: { + name: 1, + }, + }, + }).fetchAsync(); + + assert.lengthOf(data, 5); + _.each(data, (post) => { + assert.isString(post.title); + assert.isString(post._id); + assert.isObject(post.group); + assert.isString(post.group._id); + assert.isString(post.group.name); }); - - it("Should fetch Many-Meta links correctly", function() { - const data = createQuery({ - authors: { - name: 1, - groups: { - $options: { limit: 1 }, - name: 1 - } - } - }).fetch(); - - _.each(data, author => { - assert.isArray(author.groups); - assert.lengthOf(author.groups, 1); - - _.each(author.groups, group => { - assert.isObject(group); - assert.isString(group._id); - assert.isString(group.name); - }); - }); + }); + + it('Should fetch One-Meta inversed links correctly', async function () { + const data = await createQuery({ + groups: { + name: 1, + posts: { + title: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (group) => { + assert.isString(group.name); + assert.isString(group._id); + assert.lengthOf(_.keys(group), 3); + assert.isArray(group.posts); + _.each(group.posts, (post) => { + assert.isString(post.title); + assert.isString(post._id); + }); }); + }); + + it('Should fetch Many-Meta links correctly', async function () { + const data = await createQuery({ + authors: { + name: 1, + groups: { + $options: { limit: 1 }, + name: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (author) => { + assert.isArray(author.groups); + assert.lengthOf(author.groups, 1); + + _.each(author.groups, (group) => { + assert.isObject(group); + assert.isString(group._id); + assert.isString(group.name); + }); + }); + }); + + it('Should fetch Many-Meta links correctly where parent is One link', async function () { + const data = await createQuery({ + posts: { + $options: { limit: 5 }, + author: { + groups: { + isAdmin: 1, + }, + }, + }, + }).fetchAsync(); - it("Should fetch Many-Meta links correctly where parent is One link", function() { - const data = createQuery({ - posts: { - $options: { limit: 5 }, - author: { - groups: { - isAdmin: 1 - } - } - } - }).fetch(); - - // console.log(JSON.stringify(data, null, 2)); + // console.log(JSON.stringify(data, null, 2)); - _.each(data, post => { - assert.isObject(post.author); - assert.isArray(post.author.groups); + _.each(data, (post) => { + assert.isObject(post.author); + assert.isArray(post.author.groups); - _.each(post.author.groups, group => { - assert.isObject(group.$metadata); - assert.isBoolean(group.$metadata.isAdmin); - }); - }); + _.each(post.author.groups, (group) => { + assert.isObject(group.$metadata); + assert.isBoolean(group.$metadata.isAdmin); + }); }); - - it("Should fetch Many-Meta inversed links correctly", function() { - const data = createQuery({ - groups: { - name: 1, - authors: { - $options: { limit: 2 }, - name: 1 - } - } - }).fetch(); - - _.each(data, group => { - assert.isArray(group.authors); - assert.isTrue(group.authors.length <= 2); - - _.each(group.authors, author => { - assert.isObject(author); - assert.isString(author._id); - assert.isString(author.name); - }); - }); + }); + + it('Should fetch Many-Meta inversed links correctly', async function () { + const data = await createQuery({ + groups: { + name: 1, + authors: { + $options: { limit: 2 }, + name: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (group) => { + assert.isArray(group.authors); + assert.isTrue(group.authors.length <= 2); + + _.each(group.authors, (author) => { + assert.isObject(author); + assert.isString(author._id); + assert.isString(author.name); + }); }); - - it("Should fetch direct One & Many Meta links with $metadata", function() { - let data = createQuery({ - posts: { - group: { - name: 1 - } - } - }).fetch(); - - _.each(data, post => { - assert.isObject(post.group.$metadata); - assert.isDefined(post.group.$metadata.random); - }); - - data = createQuery({ - authors: { - groups: { - $options: { limit: 1 }, - name: 1 - } - } - }).fetch(); - - _.each(data, author => { - assert.isArray(author.groups); - - _.each(author.groups, group => { - assert.isObject(group.$metadata); - }); - }); + }); + + it('Should fetch direct One & Many Meta links with $metadata', async function () { + let data = await createQuery({ + posts: { + group: { + name: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (post) => { + assert.isObject(post.group.$metadata); + assert.isDefined(post.group.$metadata.random); }); - it("Should fetch direct One Meta links with $metadata that are under a nesting level", function() { - let authors = createQuery({ - authors: { - $options: { limit: 1 }, - posts: { - $options: { limit: 1 }, - group: { - name: 1 - } - } - } - }).fetch(); - - let data = authors[0]; - - _.each(data.posts, post => { - assert.isObject(post.group.$metadata); - assert.isDefined(post.group.$metadata.random); - }); + data = await createQuery({ + authors: { + groups: { + $options: { limit: 1 }, + name: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (author) => { + assert.isArray(author.groups); + + _.each(author.groups, (group) => { + assert.isObject(group.$metadata); + }); }); + }); + + it('Should fetch direct One Meta links with $metadata that are under a nesting level', async function () { + let authors = await createQuery({ + authors: { + $options: { limit: 1 }, + posts: { + $options: { limit: 1 }, + group: { + name: 1, + }, + }, + }, + }).fetchAsync(); - it("Should fetch Inversed One & Many Meta links with $metadata", function() { - let data = createQuery({ - groups: { - posts: { - group_groups_meta: 1, - title: 1 - } - } - }).fetch(); - - _.each(data, group => { - _.each(group.posts, post => { - assert.isObject(post.$metadata); - assert.isDefined(post.$metadata.random); - }); - }); + let data = authors[0]; - data = createQuery({ - groups: { - authors: { - $options: { limit: 1 }, - name: 1 - } - } - }).fetch(); - - _.each(data, group => { - _.each(group.authors, author => { - assert.isObject(author.$metadata); - }); - }); + _.each(data.posts, (post) => { + assert.isObject(post.group.$metadata); + assert.isDefined(post.group.$metadata.random); + }); + }); + + it('Should fetch Inversed One & Many Meta links with $metadata', async function () { + let data = await createQuery({ + groups: { + posts: { + group_groups_meta: 1, + title: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (group) => { + _.each(group.posts, (post) => { + assert.isObject(post.$metadata); + assert.isDefined(post.$metadata.random); + }); }); - it("Should fetch in depth properly at any given level.", function() { - const data = createQuery({ - authors: { - $options: { limit: 5 }, + data = await createQuery({ + groups: { + authors: { + $options: { limit: 1 }, + name: 1, + }, + }, + }).fetchAsync(); + + _.each(data, (group) => { + _.each(group.authors, (author) => { + assert.isObject(author.$metadata); + }); + }); + }); + + it('Should fetch in depth properly at any given level.', async function () { + const data = await createQuery({ + authors: { + $options: { limit: 5 }, + posts: { + $options: { limit: 5 }, + comments: { + $options: { limit: 5 }, + author: { + groups: { posts: { - $options: { limit: 5 }, - comments: { - $options: { limit: 5 }, - author: { - groups: { - posts: { - $options: { limit: 5 }, - author: { - name: 1 - } - } - } - } - } - } - } - }).fetch(); - - assert.lengthOf(data, 5); - let arrivedInDepth = false; - - _.each(data, author => { - _.each(author.posts, post => { - _.each(post.comments, comment => { - _.each(comment.author.groups, group => { - _.each(group.posts, post => { - assert.isObject(post.author); - assert.isString(post.author.name); - arrivedInDepth = true; - }); - }); - }); - }); - }); - - assert.isTrue(arrivedInDepth); - }); - - it("Should not throw when nested fields have identical documents", function() { - Level1.remove({}); - Level2.remove({}); - Level3.remove({}); - Level4.remove({}); - - /** - * When prepareForDelivery() calls storeOneResults() in case of deep nested fields, sometimes - * it happens that in sameLevelResults are passed non-array values resulting in incorrent behaviour - * with for example null fields. - * - * Consider the example (only links are shown and all links are of type one): - * - * RootDoc1 -- - * \ - * => Child => GrandChild => GrandGrandChild - * / - * RootDoc2 -- - * - * - RootDoc1 and RootDoc2 share the *same* Child object. - * - storeOneResults() mutates objects - * - * What happens: - * 0. We fetched the results in hypernova and now we are in prepareForDeliver() calling storeOneResults() - * 1. At first: - * both RootDoc1 and RootDoc2 have an array of length 1 in Child field, - * Child has array of GrandChild of length 1 - * GrandChild has array of GrandGrandChild of length 1 - * - * 2. Since storeOneResults() is recursive it comes to the bottom of the graph first, removing array on GrandChild.GrandGrandChild - * 3. Then it removes GrandChild array on Child document. - * 4. After that it goes to the RootDoc2 instance, but since object are shared between RootDoc1 and RootDoc2, - * in recursive call result[collectionNode.linkName] is no longer array, but object. - * Therefore, _.each(sameLevelResults) now iterates over object with all kind of unwanted consequences. - * - * sampleField in example below is show how null field can force prepareToDelivery() into crash with - * error "Cannot read property 'level4' of null" - * - */ - - const level4Id = Level4.insert({title: 'Level 4'}); - const level3Id = Level3.insert({title: 'Level 3', level4Id, sampleField: null}); - const level2Id = Level2.insert({title: 'Level 2', level3Id}); - - Level1.insert({title: 'Level 1 #1', level2Id}); - Level1.insert({title: 'Level 1 #2', level2Id}); - - expect(() => { - Level1.createQuery({ - title: 1, - level2: { - title: 1, - sampleField: 1, - level3: { - title: 1, - sampleField: 1, - level4: { - title: 1, - }, - }, + $options: { limit: 5 }, + author: { + name: 1, + }, }, - }).fetch(); - }).to.not.throw(); - }); - - it("Should work with filters of $and and $or on subcollections", function() { - let data = createQuery({ - posts: { - comments: { - $filters: { - $and: [ - { - text: "Good" - } - ] - }, - text: 1 - } - } - }).fetch(); - - data.forEach(post => { - if (post.comments) { - post.comments.forEach(comment => { - assert.equal(comment.text, "Good"); - }); - } + }, + }, + }, + }, + }, + }).fetchAsync(); + + assert.lengthOf(data, 5); + let arrivedInDepth = false; + + _.each(data, (author) => { + _.each(author.posts, (post) => { + _.each(post.comments, (comment) => { + _.each(comment.author.groups, (group) => { + _.each(group.posts, (post) => { + assert.isObject(post.author); + assert.isString(post.author.name); + arrivedInDepth = true; + }); + }); }); + }); }); - it("Should work sorting with options that contain a dot", function() { - let data = createQuery({ - posts: { - author: { - $filter({ options }) { - options.sort = { - "profile.firstName": 1 - }; - }, - profile: 1 - } - } - }).fetch(); - - assert.isArray(data); - }); - - it("Should properly clone and work with setParams", function() { - let query = createQuery({ - posts: { - $options: { limit: 5 } - } - }); - - let clone = query.clone({}); - - assert.isFunction(clone.fetch); - assert.isFunction(clone.fetchOne); - assert.isFunction(clone.setParams); - assert.isFunction(clone.setParams({}).fetchOne); + assert.isTrue(arrivedInDepth); + }); + + it('Should not throw when nested fields have identical documents', async function () { + await Level1.removeAsync({}); + await Level2.removeAsync({}); + await Level3.removeAsync({}); + await Level4.removeAsync({}); + + /** + * When prepareForDelivery() calls storeOneResults() in case of deep nested fields, sometimes + * it happens that in sameLevelResults are passed non-array values resulting in incorrent behaviour + * with for example null fields. + * + * Consider the example (only links are shown and all links are of type one): + * + * RootDoc1 -- + * \ + * => Child => GrandChild => GrandGrandChild + * / + * RootDoc2 -- + * + * - RootDoc1 and RootDoc2 share the *same* Child object. + * - storeOneResults() mutates objects + * + * What happens: + * 0. We fetched the results in hypernova and now we are in prepareForDeliver() calling storeOneResults() + * 1. At first: + * both RootDoc1 and RootDoc2 have an array of length 1 in Child field, + * Child has array of GrandChild of length 1 + * GrandChild has array of GrandGrandChild of length 1 + * + * 2. Since storeOneResults() is recursive it comes to the bottom of the graph first, removing array on GrandChild.GrandGrandChild + * 3. Then it removes GrandChild array on Child document. + * 4. After that it goes to the RootDoc2 instance, but since object are shared between RootDoc1 and RootDoc2, + * in recursive call result[collectionNode.linkName] is no longer array, but object. + * Therefore, _.each(sameLevelResults) now iterates over object with all kind of unwanted consequences. + * + * sampleField in example below is show how null field can force prepareToDelivery() into crash with + * error "Cannot read property 'level4' of null" + * + */ + + const level4Id = await Level4.insertAsync({ title: 'Level 4' }); + const level3Id = await Level3.insertAsync({ + title: 'Level 3', + level4Id, + sampleField: null, }); + const level2Id = await Level2.insertAsync({ title: 'Level 2', level3Id }); + + await Level1.insertAsync({ title: 'Level 1 #1', level2Id }); + await Level1.insertAsync({ title: 'Level 1 #2', level2Id }); + + expect(async () => { + await Level1.createQuery({ + title: 1, + level2: { + title: 1, + sampleField: 1, + level3: { + title: 1, + sampleField: 1, + level4: { + title: 1, + }, + }, + }, + }).fetchAsync(); + }).to.not.throw(); + }); + + it('Should work with filters of $and and $or on subcollections', async function () { + let data = await createQuery({ + posts: { + comments: { + $filters: { + $and: [ + { + text: 'Good', + }, + ], + }, + text: 1, + }, + }, + }).fetchAsync(); - it("Should work with $postFilters", function() { - let query = createQuery({ - posts: { - $postFilters: { - "comments.text": "Non existing comment" - }, - title: 1, - comments: { - text: 1 - } - } - }); - - const data = query.fetch(); - assert.lengthOf(data, 0); - - query = createQuery({ - posts: { - $postFilters: { - "comments.text": "Good" - }, - title: 1, - comments: { - text: 1 - } - } + data.forEach((post) => { + if (post.comments) { + post.comments.forEach((comment) => { + assert.equal(comment.text, 'Good'); }); - - assert.isTrue(query.fetch().length > 0); + } }); - - it("Should work with $postOptions", function() { - let query = createQuery({ - posts: { - $postOptions: { - limit: 5, - skip: 5, - sort: { title: 1 } - }, - title: 1, - comments: { - text: 1 - } - } - }); - - const data = query.fetch(); - assert.lengthOf(data, 5); + }); + + it('Should work sorting with options that contain a dot', async function () { + let data = await createQuery({ + posts: { + author: { + $filter({ options }) { + options.sort = { + 'profile.firstName': 1, + }; + }, + profile: 1, + }, + }, + }).fetchAsync(); + + assert.isArray(data); + }); + + it('Should properly clone and work with setParams', function () { + let query = createQuery({ + posts: { + $options: { limit: 5 }, + }, }); - it("Should work with $postFilter and params", function(done) { - let query = createQuery({ - posts: { - $postFilter(results, params) { - assert.equal(params.text, "Good"); - done(); - }, - title: 1, - comments: { - text: 1 - } - } - }); - - query.setParams({ - text: "Good" - }); - - query.fetch(); + let clone = query.clone({}); + + assert.isFunction(clone.fetch); + assert.isFunction(clone.fetchOne); + assert.isFunction(clone.setParams); + assert.isFunction(clone.setParams({}).fetchOne); + }); + + it('Should work with $postFilters', async function () { + let query = createQuery({ + posts: { + $postFilters: { + 'comments.text': 'Non existing comment', + }, + title: 1, + comments: { + text: 1, + }, + }, }); - it("Should work with a nested field from reversedSide using aggregation framework", function() { - let query = createQuery({ - groups: { - $options: { limit: 1 }, - authors: { - profile: { - firstName: 1 - } - } - } - }); - - const data = query.fetch(); - assert.lengthOf(data, 1); - - const group = data[0]; - - assert.isArray(group.authors); - assert.isTrue(group.authors.length > 0); - - const author = group.authors[0]; - assert.isObject(author); - assert.isObject(author.profile); - assert.isString(author.profile.firstName); - assert.isUndefined(author.profile.lastName); + const data = await query.fetchAsync(); + assert.lengthOf(data, 0); + + query = createQuery({ + posts: { + $postFilters: { + 'comments.text': 'Good', + }, + title: 1, + comments: { + text: 1, + }, + }, }); - it("Should apply a default filter function to first root", function() { - let query = createQuery( - { - groups: { - authors: {} - } - }, - { - params: { - options: { limit: 1 }, - filters: { - name: "JavaScript" - } - } - } - ); - - const data = query.fetch(); - assert.lengthOf(data, 1); - const group = data[0]; - assert.isArray(group.authors); - assert.isTrue(group.authors.length > 0); - }); - - Restaurants.addLinks({ - users: { - type: "many", - field: "userIds", - collection: Users - } - }); - - Users.addLinks({ - restaurants: { - collection: Restaurants, - inversedBy: "users" - } - }); - - it("Should fetch Many - inversed links correctly when the field is not the first", function() { - const userId1 = Users.insert({ - name: "John" - }); - const userId2 = Users.insert({ - name: "John" - }); - - const restaurantId = Restaurants.insert({ - name: "Jamie Oliver", - userIds: [userId2, userId1] - }); - - const user = Users.createQuery({ - $filters: { - _id: userId1 - }, - restaurants: { - name: 1 - } - }).fetchOne(); - - assert.isObject(user); - assert.isArray(user.restaurants); - assert.lengthOf(user.restaurants, 1); + assert.isTrue((await query.fetchAsync()).length > 0); + }); + + it('Should work with $postOptions', async function () { + let query = createQuery({ + posts: { + $postOptions: { + limit: 5, + skip: 5, + sort: { title: 1 }, + }, + title: 1, + comments: { + text: 1, + }, + }, }); - it("Should fetch deeply nested fields inside links", function() { - const query = createQuery({ - authors: { - posts: { - metadata: { - language: { - abbr: 1 - } - } - } - } - }); - - const data = query.fetch(); - - assert.isTrue(data.length > 0); - - data.forEach(author => { - author.posts.forEach(post => { - assert.isObject(post.metadata); - assert.isObject(post.metadata.language); - assert.isDefined(post.metadata.language.abbr); - }); - }); + const data = await query.fetchAsync(); + assert.lengthOf(data, 5); + }); + + it('Should work with $postFilter and params', async function (done) { + let query = createQuery({ + posts: { + $postFilter(results, params) { + assert.equal(params.text, 'Good'); + done(); + }, + title: 1, + comments: { + text: 1, + }, + }, }); - it("Should handle empty inversedBy meta unique fields", () => { - ShoppingCart.remove({}); - ShoppingCart.insert({ - date: new Date(), - items: [{ title: "Something" }] - }); - - const data = ShoppingCart.createQuery({ - user: { - name: 1 - } - }).fetch(); - - assert.equal(data.length, 1); - const [cart] = data; - assert.isUndefined(cart.user); + query.setParams({ + text: 'Good', }); - it("Should remove link storage inversedBy meta unique fields", () => { - ShoppingCart.remove({}); - const cartId = ShoppingCart.insert({ - date: new Date(), - items: [{ title: "Something" }] - }); - - Clients.remove({}); - Clients.insert({ - name: "John", - shoppingCartData: { - prime: 1, - _id: cartId - } - }); - - const data = ShoppingCart.createQuery({ - user: { - name: 1 - } - }).fetch(); - - assert.equal(data.length, 1); - const [cart] = data; - assert.isObject(cart.user); - assert.isString(cart.user.name); - // no link storage - assert.isUndefined(cart.user.shoppingCartData); - }); - - it("Should remove link storage inversedBy meta many fields", () => { - ShoppingCart.remove({}); - const cartId = ShoppingCart.insert({ - date: new Date(), - items: [{ title: "Something" }] - }); - - Clients.remove({}); - Clients.insert({ - name: "John", - shoppingCartsData: [ - { - prime: 1, - _id: cartId - } - ] - }); - - const data = ShoppingCart.createQuery({ - users: { - name: 1 - } - }).fetch(); - - assert.equal(data.length, 1); - const [cart] = data; - assert.isArray(cart.users); - assert.equal(cart.users.length, 1); - const [user] = cart.users; - assert.isString(user.name); - // no link storage - assert.isUndefined(user.shoppingCartsData); - }); - - it("Should be able to work with custom $filter function and using $and", () => { - ShoppingCart.remove({}); - ShoppingCart.insert({ value: 1 }); - ShoppingCart.insert({ value: 2 }); - ShoppingCart.insert({ value: 3 }); - ShoppingCart.insert({ value: 4 }); - - const data = ShoppingCart.createQuery( - { - $filter({ filters, params }) { - let $or = []; - params.values.forEach(v => $or.push({ value: v })); - - filters.$or = $or; - } - }, - { - params: { - values: [1, 2] - } - } - ).fetch(); - - assert.lengthOf(data, 2); - }); - - it("$filter should work with Date objects when filtering linked items", () => { - const CartItems = new Mongo.Collection(`CartItems`); - CartItems.addLinks({ - 'shoppingCart': { - type: "one", - collection: ShoppingCart, - field: 'shoppingCartId', - } - }); - ShoppingCart.addLinks({ - 'items': { - collection: CartItems, - inversedBy: "shoppingCart", - } - }); - ShoppingCart.remove({}); - const cartId = ShoppingCart.insert({ - value: 1, - }); - CartItems.insert({ - createdAt: new Date('2010-01-01T00:00:00'), - shoppingCartId: cartId, - name: 'item1', - }); - CartItems.insert({ - createdAt: new Date('2019-08-25T00:00:00'), - shoppingCartId: cartId, - name: 'item2', - }); - - - const data = ShoppingCart.createQuery( - { - value: 1, - items: { - $filter({filters, params}) { - filters.createdAt = { - $gte: new Date('2019-01-01T00:00:00'), - $lte: new Date('2019-12-31T23:59:59') - } - }, - name: 1, - }, - } - ).fetch(); - assert.lengthOf(data, 1); - assert.lengthOf(data[0].items, 1); - assert.equal(data[0].items[0].name, "item2"); - }); - - it("It should not crash when links do not exist", () => { - const id = `shouldNotCrash_${Random.id()}`; - const A = new Mongo.Collection(`${id}_a`); - const B = new Mongo.Collection(`${id}_b`); - const C = new Mongo.Collection(`${id}_c`); - - A.addLinks({ - b: { - field: "bLinks", - collection: B, - type: "many", - metadata: true - } - }); - - C.addLinks({ - b: { - field: "bLinks", - collection: B, - type: "many", - metadata: true - } - }); - - B.addLinks({ - a: { - collection: A, - inversedBy: "b" - }, - c: { - collection: C, - inversedBy: "b" - } - }); + await query.fetchAsync(); + }); - const bId = B.insert({}); - const cId = C.insert({ bLinks: [{ _id: "unknownId" }, { _id: bId }] }); - - const result = C.createQuery({ - b: { - a: { - _id: 1 - } - } - }).fetchOne(); // Throws, because there is no "b" with _id 'unknownId' - - expect(result).to.not.equal(undefined); - }); - - it("Should work with links on nested fields - one", () => { - const result = Files.createQuery({ - filename: 1, - meta: 1, - project: { - name: 1 - } - // todo: - // Put the meta: 1 here below project and meta.projectId will be cleared. This is because - // _shouldCleanStorage processes project fieldNode before meta fieldNode. - // Problem is manifested in collectionNode.js hasField when iterating over this.fieldNodes - // Potential solution is to process field nodes first and then linkers and reducers - }).fetchOne(); - - expect(result).to.be.an("object"); - expect(result.project).to.be.an("object"); - expect(result.project.name).to.be.equal("Project 1"); - expect(result.meta).to.be.an("object"); - expect(result.meta.type).to.be.equal("text"); - expect(result.meta.projectId).to.be.a("string"); - }); - - it("Should work with links on nested fields inside nested fields - one", () => { - const result = Files.createQuery({ - filename: 1, - meta: { - type: 1, - project: { - name: 1, - }, - }, - }).fetchOne(); - - // console.log('result', result); - - expect(result).to.be.an('object'); - expect(result.meta).to.be.an('object'); - expect(result.meta.project).to.be.an('object'); - expect(result.meta.project.name).to.be.equal('Project 1'); + it('Should work with a nested field from reversedSide using aggregation framework', async function () { + let query = createQuery({ + groups: { + $options: { limit: 1 }, + authors: { + profile: { + firstName: 1, + }, + }, + }, }); - it("Should work with links on nested fields inside nested fields (array) - one", () => { - const result = Files.createQuery({ - filename: 1, - metas: { - type: 1, - project: { - name: 1, - }, - }, - }).fetchOne(); + const data = await query.fetchAsync(); + assert.lengthOf(data, 1); + + const group = data[0]; + + assert.isArray(group.authors); + assert.isTrue(group.authors.length > 0); + + const author = group.authors[0]; + assert.isObject(author); + assert.isObject(author.profile); + assert.isString(author.profile.firstName); + assert.isUndefined(author.profile.lastName); + }); + + it('Should apply a default filter function to first root', async function () { + let query = createQuery( + { + groups: { + authors: {}, + }, + }, + { + params: { + options: { limit: 1 }, + filters: { + name: 'JavaScript', + }, + }, + }, + ); + + const data = await query.fetchAsync(); + assert.lengthOf(data, 1); + const group = data[0]; + assert.isArray(group.authors); + assert.isTrue(group.authors.length > 0); + }); + + Restaurants.addLinks({ + users: { + type: 'many', + field: 'userIds', + collection: Users, + }, + }); - // console.log('result', result); + Users.addLinks({ + restaurants: { + collection: Restaurants, + inversedBy: 'users', + }, + }); - expect(result).to.be.an('object'); - expect(result.metas).to.be.an('array'); - expect(result.metas[0].project).to.be.an('object'); - expect(result.metas[0].project.name).to.be.equal('Project 1'); - expect(result.metas[1].project.name).to.be.equal('Project 2'); + it('Should fetch Many - inversed links correctly when the field is not the first', async function () { + const userId1 = await Users.insertAsync({ + name: 'John', + }); + const userId2 = await Users.insertAsync({ + name: 'John', }); - it("Should work with links on nested fields - one (w/o meta)", () => { - const result = Files.createQuery({ - filename: 1, - project: { - name: 1 - } - }).fetchOne(); - - expect(result).to.be.an("object"); - expect(result.meta).to.be.eql({}); // {} - not yet supporting clearing of empty storage + const restaurantId = await Restaurants.insertAsync({ + name: 'Jamie Oliver', + userIds: [userId2, userId1], }); - it("Should work with links on nested fields - one inversed", () => { - const result = Projects.createQuery({ - $filters: { - name: "Project 1" + const user = await Users.createQuery({ + $filters: { + _id: userId1, + }, + restaurants: { + name: 1, + }, + }).fetchOneAsync(); + + assert.isObject(user); + assert.isArray(user.restaurants); + assert.lengthOf(user.restaurants, 1); + }); + + it('Should fetch deeply nested fields inside links', async function () { + const query = createQuery({ + authors: { + posts: { + metadata: { + language: { + abbr: 1, }, - name: 1, - files: { - filename: 1, - meta: 1 - } - }).fetchOne(); - - expect(result).to.be.an("object"); - expect(result.files).to.be.an("array"); - expect(result.files).to.have.length(2); - result.files.forEach(file => { - expect(file._id).to.be.a("string"); - expect(file.filename).to.be.a("string"); - expect(file.meta).to.be.an("object"); - expect(_.keys(file.meta)).to.be.eql(["type", "projectId", "projectIds"]); - }); + }, + }, + }, }); - it("Should work with links on nested fields - many", () => { - const result = Files.createQuery({ - filename: 1, - metas: 1, - projects: { - name: 1 - } - // todo: see comment for meta: 1 above - }).fetch(); - - expect(result).to.be.an("array"); - expect(result).to.have.length(2); - - const [res1, res2] = result; - - expect(res1.projects).to.be.an("array"); - expect(res1.projects).to.have.length(2); + const data = await query.fetchAsync(); - const [project1, project2] = res1.projects; - expect(project1.name).to.be.equal("Project 1"); - expect(project2.name).to.be.equal("Project 2"); - expect(res1.metas).to.be.an("array"); - expect(_.keys(res1.metas[0])).to.be.eql(["type", "projectId", "projectIds"]); + assert.isTrue(data.length > 0); - expect(res2.projects).to.be.an("array"); - expect(res2.projects).to.have.length(1); - expect(res2.metas).to.be.an("array"); - expect(_.keys(res2.metas[0])).to.be.eql(["type", "projectId"]); - - const [project] = res2.projects; - expect(project.name).to.be.equal("Project 2"); + data.forEach((author) => { + author.posts.forEach((post) => { + assert.isObject(post.metadata); + assert.isObject(post.metadata.language); + assert.isDefined(post.metadata.language.abbr); + }); }); + }); - it('Should work with links on nested fields inside nested fields - many in object', () => { - const result = Files.createQuery({ - filename: 1, - meta: { - manyProjects: { - name: 1, - }, - }, - }).fetch(); - - // console.log('result', JSON.stringify(result)); - - expect(result).to.be.an("array"); - expect(result).to.have.length(2); - - result.forEach(file => { - expect(file.meta).to.be.an('object'); - expect(file.meta.manyProjects).to.be.an('array'); - expect(file.meta.manyProjects.length).to.be.greaterThan(0); - }); + it('Should handle empty inversedBy meta unique fields', async () => { + await ShoppingCart.removeAsync({}); + await ShoppingCart.insertAsync({ + date: new Date(), + items: [{ title: 'Something' }], }); - it('Should work with links on nested fields inside nested fields - many in array', () => { - const result = Files.createQuery({ - filename: 1, - metas: { - manyProjects: { - name: 1, - }, - }, - }).fetch(); - - expect(result).to.be.an("array"); - expect(result).to.have.length(2); - - result.forEach(file => { - if (file.filename === 'test.txt') { - expect(file.metas).to.be.an('array').and.have.length(2); - expect(file.metas[0].manyProjects.map((p) => ({ - name: p.name - }))).to.be.eql([ - { - name: 'Project 1' - } - ]); - expect(file.metas[1].manyProjects).to.have.length(2); - } - // invoice.pdf file - else { - expect(file.metas).to.be.an('array').and.have.length(1); - expect(file.metas[0].manyProjects).to.be.an('array').and.have.length(0); - } - }); + const data = await ShoppingCart.createQuery({ + user: { + name: 1, + }, + }).fetchAsync(); + + assert.equal(data.length, 1); + const [cart] = data; + assert.isUndefined(cart.user); + }); + + it('Should remove link storage inversedBy meta unique fields', async () => { + await ShoppingCart.removeAsync({}); + const cartId = await ShoppingCart.insertAsync({ + date: new Date(), + items: [{ title: 'Something' }], }); - it("Should work with links on nested fields - many (w/o metas)", () => { - const result = Files.createQuery({ - filename: 1, - projects: { - name: 1 - } - }).fetch(); + await Clients.removeAsync({}); + await Clients.insertAsync({ + name: 'John', + shoppingCartData: { + prime: 1, + _id: cartId, + }, + }); - expect(result).to.be.an("array"); - expect(result).to.have.length(2); + const data = await ShoppingCart.createQuery({ + user: { + name: 1, + }, + }).fetchAsync(); + + assert.equal(data.length, 1); + const [cart] = data; + assert.isObject(cart.user); + assert.isString(cart.user.name); + // no link storage + assert.isUndefined(cart.user.shoppingCartData); + }); + + it('Should remove link storage inversedBy meta many fields', async () => { + await ShoppingCart.removeAsync({}); + const cartId = await ShoppingCart.insertAsync({ + date: new Date(), + items: [{ title: 'Something' }], + }); - const [res1, res2] = result; - expect(res1.metas).to.be.eql([{}, {}]); - expect(res2.metas).to.be.eql([{}]); + await Clients.removeAsync({}); + await Clients.insertAsync({ + name: 'John', + shoppingCartsData: [ + { + prime: 1, + _id: cartId, + }, + ], }); - it("Should work with links on nested fields - many inversed", () => { - const result = Projects.createQuery({ - filename: 1, - filesMany: { - filename: 1, - metas: 1 - // todo: - // Unrelated to nested fields probably - // Try metas: {type: 1} and the returned results will be metas: {type: [....]} - // Problem is in buildAggregatePipeline and snapBackDottedFields - } - }).fetch(); + const data = await ShoppingCart.createQuery({ + users: { + name: 1, + }, + }).fetchAsync(); + + assert.equal(data.length, 1); + const [cart] = data; + assert.isArray(cart.users); + assert.equal(cart.users.length, 1); + const [user] = cart.users; + assert.isString(user.name); + // no link storage + assert.isUndefined(user.shoppingCartsData); + }); + + it('Should be able to work with custom $filter function and using $and', async () => { + await ShoppingCart.removeAsync({}); + await ShoppingCart.insertAsync({ value: 1 }); + await ShoppingCart.insertAsync({ value: 2 }); + await ShoppingCart.insertAsync({ value: 3 }); + await ShoppingCart.insertAsync({ value: 4 }); + + const data = await ShoppingCart.createQuery( + { + $filter({ filters, params }) { + let $or = []; + params.values.forEach((v) => $or.push({ value: v })); + + filters.$or = $or; + }, + }, + { + params: { + values: [1, 2], + }, + }, + ).fetchAsync(); + + assert.lengthOf(data, 2); + }); + + it('$filter should work with Date objects when filtering linked items', async () => { + const CartItems = new Mongo.Collection(`CartItems`); + CartItems.addLinks({ + shoppingCart: { + type: 'one', + collection: ShoppingCart, + field: 'shoppingCartId', + }, + }); + ShoppingCart.addLinks({ + items: { + collection: CartItems, + inversedBy: 'shoppingCart', + }, + }); + await ShoppingCart.removeAsync({}); + const cartId = await ShoppingCart.insertAsync({ + value: 1, + }); + await CartItems.insertAsync({ + createdAt: new Date('2010-01-01T00:00:00'), + shoppingCartId: cartId, + name: 'item1', + }); + await CartItems.insertAsync({ + createdAt: new Date('2019-08-25T00:00:00'), + shoppingCartId: cartId, + name: 'item2', + }); - expect(result).to.be.an("array"); - expect(result).to.have.length(2); + const data = await ShoppingCart.createQuery({ + value: 1, + items: { + $filter({ filters, params }) { + filters.createdAt = { + $gte: new Date('2019-01-01T00:00:00'), + $lte: new Date('2019-12-31T23:59:59'), + }; + }, + name: 1, + }, + }).fetchAsync(); + assert.lengthOf(data, 1); + assert.lengthOf(data[0].items, 1); + assert.equal(data[0].items[0].name, 'item2'); + }); + + it('It should not crash when links do not exist', async () => { + const id = `shouldNotCrash_${Random.id()}`; + const A = new Mongo.Collection(`${id}_a`); + const B = new Mongo.Collection(`${id}_b`); + const C = new Mongo.Collection(`${id}_c`); + + A.addLinks({ + b: { + field: 'bLinks', + collection: B, + type: 'many', + metadata: true, + }, + }); - const [res1, res2] = result; + C.addLinks({ + b: { + field: 'bLinks', + collection: B, + type: 'many', + metadata: true, + }, + }); - expect(res1.filesMany).to.be.an("array"); - expect(res1.filesMany).to.have.length(1); - const [file] = res1.filesMany; - expect(file.filename).to.be.equal("test.txt"); + B.addLinks({ + a: { + collection: A, + inversedBy: 'b', + }, + c: { + collection: C, + inversedBy: 'b', + }, + }); - expect(res2.filesMany).to.be.an("array"); - expect(res2.filesMany).to.have.length(2); + const bId = await B.insertAsync({}); + const cId = await C.insertAsync({ + bLinks: [{ _id: 'unknownId' }, { _id: bId }], + }); - const [file1, file2] = res2.filesMany; - expect(file1.filename).to.be.a("string"); - expect(file2.filename).to.be.a("string"); + const result = await C.createQuery({ + b: { + a: { + _id: 1, + }, + }, + }).fetchOneAsync(); // Throws, because there is no "b" with _id 'unknownId' + + expect(result).to.not.equal(undefined); + }); + + it('Should work with links on nested fields - one', async () => { + const result = await Files.createQuery({ + filename: 1, + meta: 1, + project: { + name: 1, + }, + // todo: + // Put the meta: 1 here below project and meta.projectId will be cleared. This is because + // _shouldCleanStorage processes project fieldNode before meta fieldNode. + // Problem is manifested in collectionNode.js hasField when iterating over this.fieldNodes + // Potential solution is to process field nodes first and then linkers and reducers + }).fetchOneAsync(); + + expect(result).to.be.an('object'); + expect(result.project).to.be.an('object'); + expect(result.project.name).to.be.equal('Project 1'); + expect(result.meta).to.be.an('object'); + expect(result.meta.type).to.be.equal('text'); + expect(result.meta.projectId).to.be.a('string'); + }); + + it('Should work with links on nested fields inside nested fields - one', async () => { + const result = await Files.createQuery({ + filename: 1, + meta: { + type: 1, + project: { + name: 1, + }, + }, + }).fetchOneAsync(); + + // console.log('result', result); + + expect(result).to.be.an('object'); + expect(result.meta).to.be.an('object'); + expect(result.meta.project).to.be.an('object'); + expect(result.meta.project.name).to.be.equal('Project 1'); + }); + + it('Should work with links on nested fields inside nested fields (array) - one', async () => { + const result = await Files.createQuery({ + filename: 1, + metas: { + type: 1, + project: { + name: 1, + }, + }, + }).fetchOneAsync(); + + // console.log('result', result); + + expect(result).to.be.an('object'); + expect(result.metas).to.be.an('array'); + expect(result.metas[0].project).to.be.an('object'); + expect(result.metas[0].project.name).to.be.equal('Project 1'); + expect(result.metas[1].project.name).to.be.equal('Project 2'); + }); + + it('Should work with links on nested fields - one (w/o meta)', async () => { + const result = await Files.createQuery({ + filename: 1, + project: { + name: 1, + }, + }).fetchOneAsync(); + + expect(result).to.be.an('object'); + expect(result.meta).to.be.eql({}); // {} - not yet supporting clearing of empty storage + }); + + it('Should work with links on nested fields - one inversed', async () => { + const result = await Projects.createQuery({ + $filters: { + name: 'Project 1', + }, + name: 1, + files: { + filename: 1, + meta: 1, + }, + }).fetchOneAsync(); + + expect(result).to.be.an('object'); + expect(result.files).to.be.an('array'); + expect(result.files).to.have.length(2); + result.files.forEach((file) => { + expect(file._id).to.be.a('string'); + expect(file.filename).to.be.a('string'); + expect(file.meta).to.be.an('object'); + expect(_.keys(file.meta)).to.be.eql(['type', 'projectId', 'projectIds']); + }); + }); + + it('Should work with links on nested fields - many', async () => { + const result = await Files.createQuery({ + filename: 1, + metas: 1, + projects: { + name: 1, + }, + // todo: see comment for meta: 1 above + }).fetchAsync(); + + expect(result).to.be.an('array'); + expect(result).to.have.length(2); + + const [res1, res2] = result; + + expect(res1.projects).to.be.an('array'); + expect(res1.projects).to.have.length(2); + + const [project1, project2] = res1.projects; + expect(project1.name).to.be.equal('Project 1'); + expect(project2.name).to.be.equal('Project 2'); + expect(res1.metas).to.be.an('array'); + expect(_.keys(res1.metas[0])).to.be.eql([ + 'type', + 'projectId', + 'projectIds', + ]); + + expect(res2.projects).to.be.an('array'); + expect(res2.projects).to.have.length(1); + expect(res2.metas).to.be.an('array'); + expect(_.keys(res2.metas[0])).to.be.eql(['type', 'projectId']); + + const [project] = res2.projects; + expect(project.name).to.be.equal('Project 2'); + }); + + it('Should work with links on nested fields inside nested fields - many in object', async () => { + const result = await Files.createQuery({ + filename: 1, + meta: { + manyProjects: { + name: 1, + }, + }, + }).fetchAsync(); + + // console.log('result', JSON.stringify(result)); + + expect(result).to.be.an('array'); + expect(result).to.have.length(2); + + result.forEach((file) => { + expect(file.meta).to.be.an('object'); + expect(file.meta.manyProjects).to.be.an('array'); + expect(file.meta.manyProjects.length).to.be.greaterThan(0); + }); + }); + + it('Should work with links on nested fields inside nested fields - many in array', async () => { + const result = await Files.createQuery({ + filename: 1, + metas: { + manyProjects: { + name: 1, + }, + }, + }).fetchAsync(); + + expect(result).to.be.an('array'); + expect(result).to.have.length(2); + + result.forEach((file) => { + if (file.filename === 'test.txt') { + expect(file.metas).to.be.an('array').and.have.length(2); + expect( + file.metas[0].manyProjects.map((p) => ({ + name: p.name, + })), + ).to.be.eql([ + { + name: 'Project 1', + }, + ]); + expect(file.metas[1].manyProjects).to.have.length(2); + } + // invoice.pdf file + else { + expect(file.metas).to.be.an('array').and.have.length(1); + expect(file.metas[0].manyProjects).to.be.an('array').and.have.length(0); + } }); + }); + + it('Should work with links on nested fields - many (w/o metas)', async () => { + const result = await Files.createQuery({ + filename: 1, + projects: { + name: 1, + }, + }).fetchAsync(); + + expect(result).to.be.an('array'); + expect(result).to.have.length(2); + + const [res1, res2] = result; + expect(res1.metas).to.be.eql([{}, {}]); + expect(res2.metas).to.be.eql([{}]); + }); + + it('Should work with links on nested fields - many inversed', async () => { + const result = await Projects.createQuery({ + filename: 1, + filesMany: { + filename: 1, + metas: 1, + // todo: + // Unrelated to nested fields probably + // Try metas: {type: 1} and the returned results will be metas: {type: [....]} + // Problem is in buildAggregatePipeline and snapBackDottedFields + }, + }).fetchAsync(); + + expect(result).to.be.an('array'); + expect(result).to.have.length(2); + + const [res1, res2] = result; + + expect(res1.filesMany).to.be.an('array'); + expect(res1.filesMany).to.have.length(1); + const [file] = res1.filesMany; + expect(file.filename).to.be.equal('test.txt'); + + expect(res2.filesMany).to.be.an('array'); + expect(res2.filesMany).to.have.length(2); + + const [file1, file2] = res2.filesMany; + expect(file1.filename).to.be.a('string'); + expect(file2.filename).to.be.a('string'); + }); }); -describe("intersectDeep", () => { - it("works - keeps $filter and does not include client fields", () => { - const allowedBody = { - $filter() {}, - $options: {}, - name: 1, - dob: 1 - }; - - const res = intersectDeep(allowedBody, { name: 1, salary: 1 }); - - expect(res).to.be.an("object"); - expect(res.name).to.be.equal(1); - expect(res.dob).to.be.undefined; - expect(res.salary).to.be.undefined; - expect(res.$filter).to.be.equal(allowedBody.$filter); - expect(res.$options).to.be.equal(allowedBody.$options); +describe('intersectDeep', () => { + it('works - keeps $filter and does not include client fields', () => { + const allowedBody = { + $filter() {}, + $options: {}, + name: 1, + dob: 1, + }; + + const res = intersectDeep(allowedBody, { name: 1, salary: 1 }); + + expect(res).to.be.an('object'); + expect(res.name).to.be.equal(1); + expect(res.dob).to.be.undefined; + expect(res.salary).to.be.undefined; + expect(res.$filter).to.be.equal(allowedBody.$filter); + expect(res.$options).to.be.equal(allowedBody.$options); + }); + + it('works - ignores client special fields', () => { + const allowedBody = { + $filter() {}, + name: 1, + dob: 1, + }; + + const clientBody = { + $paginate: true, + $filters: {}, + $filter() {}, + $options: {}, + + name: 1, + }; + + const res = intersectDeep(allowedBody, clientBody); + + expect(res).to.be.an('object'); + expect(res.name).to.be.equal(1); + expect(res.$filter).to.be.equal(allowedBody.$filter); // not from clientBody + expect(res.$filters).to.be.undefined; + expect(res.$paginate).to.be.undefined; + expect(res.$options).to.be.undefined; + }); + + it('works - with nested fields 1', () => { + const allowedBody = { + nested: 1, + }; + + const clientBody = { + nested: { + title: 1, + }, + }; + + const res = intersectDeep(allowedBody, clientBody); + expect(res.nested).to.be.eql({ title: 1 }); + }); + + it('works - with nested fields 2', () => { + const allowedBody = { + nested: { + title: 1, + date: 1, + }, + }; + + const clientBody = { + nested: 1, + }; + + const res = intersectDeep(allowedBody, clientBody); + expect(res.nested).to.be.eql({ title: 1, date: 1 }); + }); + + it('works - with nested fields 3 (clearing)', () => { + const allowedBody = { + nested: { + title: 1, + date: 1, + }, + }; + + const clientBody = { + nested: { + nothing: 1, + }, + }; + + const res = intersectDeep(allowedBody, clientBody); + expect(res.nested).to.be.eql({}); + }); + + it('works - with nested fields 3 (clearing)', () => { + const allowedBody = { + nested: { + title: 1, + date: 1, + }, + }; + + const clientBody = { + nested: { + nothing: 1, + }, + }; + + const res = intersectDeep(allowedBody, clientBody); + expect(res.nested).to.be.eql({}); + }); + + it('validity checks', () => { + const allowedBody = { + title: 1, + }; + + const clientBody = { + title: 'bla', + }; + + const res = intersectDeep(allowedBody, clientBody); + expect(res).to.be.eql({}); + }); + + it('deep reducer test', async () => { + const A = new Mongo.Collection(Random.id()); + + A.addReducers({ + reducer: { + body: { + field: { + main: { min: { a: 1, b: 1 }, max: { a: 1, b: 1 } }, + second: { min: { a: 1, b: 1 }, max: { a: 1, b: 1 } }, + }, + }, + reduce: () => { + return 'hello'; + }, + }, }); - it("works - ignores client special fields", () => { - const allowedBody = { - $filter() {}, - name: 1, - dob: 1 - }; - - const clientBody = { - $paginate: true, - $filters: {}, - $filter() {}, - $options: {}, - - name: 1 - }; - - const res = intersectDeep(allowedBody, clientBody); - - expect(res).to.be.an("object"); - expect(res.name).to.be.equal(1); - expect(res.$filter).to.be.equal(allowedBody.$filter); // not from clientBody - expect(res.$filters).to.be.undefined; - expect(res.$paginate).to.be.undefined; - expect(res.$options).to.be.undefined; - }); - - it("works - with nested fields 1", () => { - const allowedBody = { - nested: 1 - }; - - const clientBody = { - nested: { - title: 1 - } - }; - - const res = intersectDeep(allowedBody, clientBody); - expect(res.nested).to.be.eql({ title: 1 }); - }); - - it("works - with nested fields 2", () => { - const allowedBody = { - nested: { - title: 1, - date: 1 - } - }; - - const clientBody = { - nested: 1 - }; - - const res = intersectDeep(allowedBody, clientBody); - expect(res.nested).to.be.eql({ title: 1, date: 1 }); - }); - - it("works - with nested fields 3 (clearing)", () => { - const allowedBody = { - nested: { - title: 1, - date: 1 - } - }; - - const clientBody = { - nested: { - nothing: 1 - } - }; - - const res = intersectDeep(allowedBody, clientBody); - expect(res.nested).to.be.eql({}); - }); - - it("works - with nested fields 3 (clearing)", () => { - const allowedBody = { - nested: { - title: 1, - date: 1 - } - }; - - const clientBody = { - nested: { - nothing: 1 - } - }; - - const res = intersectDeep(allowedBody, clientBody); - expect(res.nested).to.be.eql({}); - }); - - it("validity checks", () => { - const allowedBody = { - title: 1 - }; - - const clientBody = { - title: "bla" - }; - - const res = intersectDeep(allowedBody, clientBody); - expect(res).to.be.eql({}); - }); - - it("deep reducer test", () => { - const A = new Mongo.Collection(Random.id()); - - A.addReducers({ - reducer: { - body: { - field: { - main: { min: { a: 1, b: 1 }, max: { a: 1, b: 1 } }, - second: { min: { a: 1, b: 1 }, max: { a: 1, b: 1 } } - } - }, - reduce: () => { - return "hello"; - } - } - }); - - A.insert({ - field: { - main: { min: { a: 1, b: 2 }, max: { a: 1, b: 2 } }, - second: { min: { a: 1, b: 2 }, max: { a: 1, b: 2 } } - } - }); - - const result = A.createQuery({ - field: { main: { min: { a: 1 }, max: 1 } }, - reducer: 1 - }).fetch(); - - expect(result[0].field.main.min).to.not.equal(undefined); - expect(result[0].field.main.max).to.not.equal(undefined); // fails! - }); - - it("$filters behavior different for many-meta-inversed link", () => { - const A = new Mongo.Collection(Random.id()); - const B = new Mongo.Collection(Random.id()); - A.addLinks({ - b: { - field: "bLinks", - collection: B, - type: "many", - metadata: true - } - }); - - B.addLinks({ - a: { - collection: A, - inversedBy: "b", - type: "many", - denormalize: { - field: "aCache", - body: { _id: 1, title: 1 } - } - } - }); - const bId = B.insert({}); - - const aId1 = A.insert({ - _id: "aId1", - title: "A1", - category: 1, - bLinks: [{ _id: bId }] - }); - - const aId2 = A.insert({ - _id: "aId2", - title: "A2", - category: 2, - bLinks: [{ _id: bId }] - }); - - const aId3 = A.insert({ - _id: "aId3", - title: "A3", - category: 2, - bLinks: [{ _id: bId }] - }); - - // expect(A.createQuery({}).fetch().length).to.equal(3); - - // expect( - // A.createQuery({ $filters: { category: 2 } }).fetch().length - // ).to.equal(2); - - // expect( - // A.createQuery({ $filters: { category: undefined } }).fetch().length - // ).to.equal(3); - - // const b1 = B.createQuery({ - // a: { category: 1 } - // }).fetchOne(); - // expect(b1.a.length).to.equal(3); - - // const b2 = B.createQuery({ - // a: { category: 1, $filters: { category: 2 } } - // }).fetchOne(); - // expect(b2.a.length).to.equal(2); + await A.insertAsync({ + field: { + main: { min: { a: 1, b: 2 }, max: { a: 1, b: 2 } }, + second: { min: { a: 1, b: 2 }, max: { a: 1, b: 2 } }, + }, + }); - let $filters = { category: undefined }; + const result = await A.createQuery({ + field: { main: { min: { a: 1 }, max: 1 } }, + reducer: 1, + }).fetchAsync(); + + expect(result[0].field.main.min).to.not.equal(undefined); + expect(result[0].field.main.max).to.not.equal(undefined); // fails! + }); + + it('$filters behavior different for many-meta-inversed link', async () => { + const A = new Mongo.Collection(Random.id()); + const B = new Mongo.Collection(Random.id()); + A.addLinks({ + b: { + field: 'bLinks', + collection: B, + type: 'many', + metadata: true, + }, + }); - const b3 = B.createQuery({ - a: { title: 1, $filters } - }).fetchOne(); - expect(b3.a.length).to.equal(3); // This returns 0, but should be 3 + B.addLinks({ + a: { + collection: A, + inversedBy: 'b', + type: 'many', + denormalize: { + field: 'aCache', + body: { _id: 1, title: 1 }, + }, + }, }); + const bId = await B.insertAsync({}); - it("It should work with reverted link and objects inside array", () => { - const A = new Mongo.Collection(Random.id()); - const B = new Mongo.Collection(Random.id()); - A.addLinks({ - b: { - field: "bId", - collection: B, - } - }); + const aId1 = await A.insertAsync({ + _id: 'aId1', + title: 'A1', + category: 1, + bLinks: [{ _id: bId }], + }); - B.addLinks({ - a: { - collection: A, - inversedBy: "b", - } - }); - const bId = B.insert({}); - - const aId1 = A.insert({ - _id: "aId1", - title: "A1", - bId, - ratings: [ - { - rating: 1, - dimension: '1' - }, - { - rating: 2, - dimension: '2' - } - ] - }); + const aId2 = await A.insertAsync({ + _id: 'aId2', + title: 'A2', + category: 2, + bLinks: [{ _id: bId }], + }); - const bObj = B.createQuery({ - a: { - ratings: { - rating: 1, - dimension: 1, - } - } - }).fetchOne(); + const aId3 = await A.insertAsync({ + _id: 'aId3', + title: 'A3', + category: 2, + bLinks: [{ _id: bId }], + }); - assert.isObject(bObj); - assert.isArray(bObj.a); - assert.isArray(bObj.a[0].ratings); + // expect(A.createQuery({}).fetch().length).to.equal(3); + + // expect( + // A.createQuery({ $filters: { category: 2 } }).fetch().length + // ).to.equal(2); + + // expect( + // A.createQuery({ $filters: { category: undefined } }).fetch().length + // ).to.equal(3); + + // const b1 = B.createQuery({ + // a: { category: 1 } + // }).fetchOne(); + // expect(b1.a.length).to.equal(3); + + // const b2 = B.createQuery({ + // a: { category: 1, $filters: { category: 2 } } + // }).fetchOne(); + // expect(b2.a.length).to.equal(2); + + let $filters = { category: undefined }; + + const b3 = await B.createQuery({ + a: { title: 1, $filters }, + }).fetchOneAsync(); + expect(b3.a.length).to.equal(3); // This returns 0, but should be 3 + }); + + it('It should work with reverted link and objects inside array', async () => { + const A = new Mongo.Collection(Random.id()); + const B = new Mongo.Collection(Random.id()); + A.addLinks({ + b: { + field: 'bId', + collection: B, + }, + }); + B.addLinks({ + a: { + collection: A, + inversedBy: 'b', + }, + }); + const bId = await B.insertAsync({}); + + const aId1 = await A.insertAsync({ + _id: 'aId1', + title: 'A1', + bId, + ratings: [ + { + rating: 1, + dimension: '1', + }, + { + rating: 2, + dimension: '2', + }, + ], + }); - bObj.a[0].ratings.forEach(r => { - assert.isString(r.dimension); - assert.isNumber(r.rating); - }) + const bObj = await B.createQuery({ + a: { + ratings: { + rating: 1, + dimension: 1, + }, + }, + }).fetchOneAsync(); + + assert.isObject(bObj); + assert.isArray(bObj.a); + assert.isArray(bObj.a[0].ratings); + + bObj.a[0].ratings.forEach((r) => { + assert.isString(r.dimension); + assert.isNumber(r.rating); }); + }); }); describe('collection transforms', () => { - const TransformA = new Mongo.Collection('transformA', { transform: doc => ({ ...doc, hello: 'world1' }) }); - const TransformB = new Mongo.Collection('transformB', { transform:doc => ({ ...doc, hello: 'world2' }) }); - - TransformA.addLinks({ - b: { - collection: TransformB, - field: 'bLink', - type: 'one' - } - }); + const TransformA = new Mongo.Collection('transformA', { + transform: (doc) => ({ ...doc, hello: 'world1' }), + }); + const TransformB = new Mongo.Collection('transformB', { + transform: (doc) => ({ ...doc, hello: 'world2' }), + }); + + TransformA.addLinks({ + b: { + collection: TransformB, + field: 'bLink', + type: 'one', + }, + }); - TransformB.addLinks({ - a: { - collection: TransformA, - inversedBy: 'b' - } - }) - - beforeEach(() => { - TransformA.remove({}); - TransformB.remove({}); - const b = TransformB.insert({ value: 1 }); - const a1 = TransformA.insert({ value: 1, bLink: b }); - const a2 = TransformA.insert({ value: 2, bLink: b }); - }) - - it('Should apply transforms on direct side', () => { - const result = createQuery({ - transformB: { - value: 1, - a: { value: 1 } - } - }).fetchOne() - - expect(result.hello).to.equal('world2') - expect(result.a[0].hello).to.equal('world1') - expect(result.a[1].hello).to.equal('world1') - }) - - it('Should apply transforms on indirect side', () => { - const [result1, result2] = createQuery({ - transformA: { - value: 1, - b: { value: 1 } - } - }).fetch() - - expect(result1.hello).to.equal('world1') - expect(result1.b.hello).to.equal('world2') - expect(result2.hello).to.equal('world1') - expect(result2.b.hello).to.equal('world2') - }) - - it('Should apply transforms in a nested way', () => { - const result = createQuery({ - transformA: { - value: 1, - b: { value: 1, a: { value: 1, b: { value: 1 } } } - } - }).fetchOne() - - expect(result.hello).to.equal('world1') - expect(result.b.hello).to.equal('world2') - expect(result.b.a[0].hello).to.equal('world1') - expect(result.b.a[1].hello).to.equal('world1') - expect(result.b.a[0].b.hello).to.equal('world2') - }) + TransformB.addLinks({ + a: { + collection: TransformA, + inversedBy: 'b', + }, + }); + + beforeEach(async () => { + await TransformA.removeAsync({}); + await TransformB.removeAsync({}); + const b = await TransformB.insertAsync({ value: 1 }); + const a1 = await TransformA.insertAsync({ value: 1, bLink: b }); + const a2 = await TransformA.insertAsync({ value: 2, bLink: b }); + }); + + it('Should apply transforms on direct side', async () => { + const result = await createQuery({ + transformB: { + value: 1, + a: { value: 1 }, + }, + }).fetchOneAsync(); + + expect(result.hello).to.equal('world2'); + expect(result.a[0].hello).to.equal('world1'); + expect(result.a[1].hello).to.equal('world1'); + }); + + it('Should apply transforms on indirect side', async () => { + const [result1, result2] = await createQuery({ + transformA: { + value: 1, + b: { value: 1 }, + }, + }).fetchAsync(); + + expect(result1.hello).to.equal('world1'); + expect(result1.b.hello).to.equal('world2'); + expect(result2.hello).to.equal('world1'); + expect(result2.b.hello).to.equal('world2'); + }); + + it('Should apply transforms in a nested way', async () => { + const result = await createQuery({ + transformA: { + value: 1, + b: { value: 1, a: { value: 1, b: { value: 1 } } }, + }, + }).fetchOneAsync(); + + expect(result.hello).to.equal('world1'); + expect(result.b.hello).to.equal('world2'); + expect(result.b.a[0].hello).to.equal('world1'); + expect(result.b.a[1].hello).to.equal('world1'); + expect(result.b.a[0].b.hello).to.equal('world2'); + }); }); diff --git a/lib/scoping/client.js b/lib/scoping/client.js new file mode 100644 index 00000000..49ec2826 --- /dev/null +++ b/lib/scoping/client.js @@ -0,0 +1,38 @@ +import { LocalCollection } from 'meteor/minimongo'; + +const Connection = Meteor.connection.constructor; + +const originalSubscribe = Connection.prototype.subscribe; +Connection.prototype.subscribe = function (...args) { + const handle = originalSubscribe.apply(this, args); + + handle.scopeQuery = function () { + const query = {}; + query[`_sub_${handle.subscriptionId}`] = { + $exists: true, + }; + return query; + }; + + return handle; +}; + +// Recreate the convenience method. +Meteor.subscribe = Meteor.connection.subscribe.bind(Meteor.connection); + +const originalCompileProjection = LocalCollection._compileProjection; +LocalCollection._compileProjection = function (fields) { + const fun = originalCompileProjection(fields); + + return function (obj) { + const res = fun(obj); + + for (const field of Object.keys(res)) { + if (field.lastIndexOf('_sub_', 0) === 0) { + delete res[field]; + } + } + + return res; + }; +}; diff --git a/lib/scoping/server.js b/lib/scoping/server.js new file mode 100644 index 00000000..3109deb8 --- /dev/null +++ b/lib/scoping/server.js @@ -0,0 +1,73 @@ +export const extendPublish = (newPublishArguments) => { + // DDP Server constructor. + const Server = Object.getPrototypeOf(Meteor.server).constructor; + + const originalPublish = Server.prototype.publish; + Server.prototype.publish = async function (...args) { + // If the first argument is an object, we let the original publish function to traverse it. + if (typeof args[0] === 'object' && args[0] !== null) { + await originalPublish.apply(this, args); + return; + } + + const newArgs = await newPublishArguments.apply(this, args); + + await originalPublish.apply(this, newArgs); + }; + + // Because Meteor.publish is a bound function it remembers old + // prototype method so we have to wrap it directly as well. + const originalMeteorPublish = Meteor.publish; + Meteor.publish = function (...args) { + // If the first argument is an object, we let the original publish function to traverse it. + if (typeof args[0] === 'object' && args[0] !== null) { + originalMeteorPublish.apply(this, args); + return; + } + + const newArgs = newPublishArguments.apply(this, args); + + originalMeteorPublish.apply(this, newArgs); + }; +}; + +// Extend publish part + +extendPublish(function (name, func, options) { + const newFunc = function (...args) { + const publish = this; + + const scopeFieldName = `_sub_${publish._subscriptionId}`; + + let enabled = false; + publish.enableScope = function () { + enabled = true; + }; + + const originalAdded = publish.added; + publish.added = function (collectionName, id, fields) { + // Add our scoping field. + if (enabled) { + fields = { ...fields }; + fields[scopeFieldName] = 1; + } + + originalAdded.call(this, collectionName, id, fields); + }; + + const originalChanged = publish.changed; + publish.changed = function (collectionName, id, fields) { + // We do not allow changes to our scoping field. + if (enabled && scopeFieldName in fields) { + fields = { ...fields }; + delete fields[scopeFieldName]; + } + + originalChanged.call(this, collectionName, id, fields); + }; + + return func.apply(publish, args); + }; + + return [name, newFunc, options]; +}); diff --git a/package-lock.json b/package-lock.json index fd4da784..bad13c8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,848 +1,3489 @@ { + "name": "grapher", + "lockfileVersion": 3, "requires": true, - "lockfileVersion": 1, - "dependencies": { - "ajv": { + "packages": { + "": { + "devDependencies": { + "@types/chai": "^4.3.20", + "@types/dot-object": "^2.1.6", + "@types/lodash.clonedeep": "^4.5.9", + "@types/meteor": "^2.9.8", + "@types/mocha": "^10.0.10", + "@types/node": "^20.11.30", + "@typescript-eslint/eslint-plugin": "^7.0.2", + "@typescript-eslint/parser": "^7.0.2", + "eslint": "^8.56.0", + "mongodb": "^6.3.0", + "typescript": "^5.7.3" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "optional": true + }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "optional": true + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "optional": true + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "optional": true + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "optional": true + }, + "node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "optional": true + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.515.0.tgz", + "integrity": "sha512-e51ImjjRLzXkPEYguvGCbhWPNhoV2OGS6mKHCR940XEeImt04yE1tytYP1vXYpPICmuYgz79BV0FOC9J5N9bvg==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.515.0", + "@aws-sdk/core": "3.513.0", + "@aws-sdk/credential-provider-node": "3.515.0", + "@aws-sdk/middleware-host-header": "3.515.0", + "@aws-sdk/middleware-logger": "3.515.0", + "@aws-sdk/middleware-recursion-detection": "3.515.0", + "@aws-sdk/middleware-user-agent": "3.515.0", + "@aws-sdk/region-config-resolver": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@aws-sdk/util-endpoints": "3.515.0", + "@aws-sdk/util-user-agent-browser": "3.515.0", + "@aws-sdk/util-user-agent-node": "3.515.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.2", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.2.0", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.515.0.tgz", + "integrity": "sha512-4oGBLW476zmkdN98lAns3bObRNO+DLOfg4MDUSR6l6GYBV/zGAtoy2O/FhwYKgA2L5h2ZtElGopLlk/1Q0ePLw==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.513.0", + "@aws-sdk/middleware-host-header": "3.515.0", + "@aws-sdk/middleware-logger": "3.515.0", + "@aws-sdk/middleware-recursion-detection": "3.515.0", + "@aws-sdk/middleware-user-agent": "3.515.0", + "@aws-sdk/region-config-resolver": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@aws-sdk/util-endpoints": "3.515.0", + "@aws-sdk/util-user-agent-browser": "3.515.0", + "@aws-sdk/util-user-agent-node": "3.515.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.2", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.2.0", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.515.0.tgz", + "integrity": "sha512-zACa8LNlPUdlNUBqQRf5a3MfouLNtcBfm84v2c8M976DwJrMGONPe1QjyLLsD38uESQiXiVQRruj/b000iMXNw==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.515.0", + "@aws-sdk/core": "3.513.0", + "@aws-sdk/middleware-host-header": "3.515.0", + "@aws-sdk/middleware-logger": "3.515.0", + "@aws-sdk/middleware-recursion-detection": "3.515.0", + "@aws-sdk/middleware-user-agent": "3.515.0", + "@aws-sdk/region-config-resolver": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@aws-sdk/util-endpoints": "3.515.0", + "@aws-sdk/util-user-agent-browser": "3.515.0", + "@aws-sdk/util-user-agent-node": "3.515.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.2", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.2.0", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.515.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.515.0.tgz", + "integrity": "sha512-ScYuvaIDgip3atOJIA1FU2n0gJkEdveu1KrrCPathoUCV5zpK8qQmO/n+Fj/7hKFxeKdFbB+4W4CsJWYH94nlg==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.513.0", + "@aws-sdk/middleware-host-header": "3.515.0", + "@aws-sdk/middleware-logger": "3.515.0", + "@aws-sdk/middleware-recursion-detection": "3.515.0", + "@aws-sdk/middleware-user-agent": "3.515.0", + "@aws-sdk/region-config-resolver": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@aws-sdk/util-endpoints": "3.515.0", + "@aws-sdk/util-user-agent-browser": "3.515.0", + "@aws-sdk/util-user-agent-node": "3.515.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.2", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.2.0", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.515.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.513.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.513.0.tgz", + "integrity": "sha512-L+9DL4apWuqNKVOMJ8siAuWoRM9rZf9w1iPv8S2o83WO2jVK7E/m+rNW1dFo9HsA5V1ccDl2H2qLXx24HiHmOw==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/core": "^1.3.2", + "@smithy/protocol-http": "^3.1.1", + "@smithy/signature-v4": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.515.0.tgz", + "integrity": "sha512-pWMJFhNc6bLbCpKhYXWWa23wMyhpFFyw3kF/6ea+95JQHF0FY2l4wDQa7ynE4hW4Wf5oA3Sf7Wf87pp9iAHubQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.515.0.tgz", + "integrity": "sha512-45vxdyqhTAaUMERYVWOziG3K8L2TV9G4ryQS/KZ84o7NAybE9GMdoZRVmGHAO7mJJ1wQiYCM/E+i5b3NW9JfNA==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.515.0.tgz", + "integrity": "sha512-Ba6FXK77vU4WyheiamNjEuTFmir0eAXuJGPO27lBaA8g+V/seXGHScsbOG14aQGDOr2P02OPwKGZrWWA7BFpfQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-stream": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.515.0.tgz", + "integrity": "sha512-ouDlNZdv2TKeVEA/YZk2+XklTXyAAGdbWnl4IgN9ItaodWI+lZjdIoNC8BAooVH+atIV/cZgoGTGQL7j2TxJ9A==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/client-sts": "3.515.0", + "@aws-sdk/credential-provider-env": "3.515.0", + "@aws-sdk/credential-provider-process": "3.515.0", + "@aws-sdk/credential-provider-sso": "3.515.0", + "@aws-sdk/credential-provider-web-identity": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.515.0.tgz", + "integrity": "sha512-Y4kHSpbxksiCZZNcvsiKUd8Fb2XlyUuONEwqWFNL82ZH6TCCjBGS31wJQCSxBHqYcOL3tiORUEJkoO7uS30uQA==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.515.0", + "@aws-sdk/credential-provider-http": "3.515.0", + "@aws-sdk/credential-provider-ini": "3.515.0", + "@aws-sdk/credential-provider-process": "3.515.0", + "@aws-sdk/credential-provider-sso": "3.515.0", + "@aws-sdk/credential-provider-web-identity": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.515.0.tgz", + "integrity": "sha512-pSjiOA2FM63LHRKNDvEpBRp80FVGT0Mw/gzgbqFXP+sewk0WVonYbEcMDTJptH3VsLPGzqH/DQ1YL/aEIBuXFQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.515.0.tgz", + "integrity": "sha512-j7vUkiSmuhpBvZYoPTRTI4ePnQbiZMFl6TNhg9b9DprC1zHkucsZnhRhqjOVlrw/H6J4jmcPGcHHTZ5WQNI5xQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/client-sso": "3.515.0", + "@aws-sdk/token-providers": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.515.0.tgz", + "integrity": "sha512-66+2g4z3fWwdoGReY8aUHvm6JrKZMTRxjuizljVmMyOBttKPeBYXvUTop/g3ZGUx1f8j+C5qsGK52viYBvtjuQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/client-sts": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.515.0.tgz", + "integrity": "sha512-XQ9maVLTtv6iJbOYiRS+IvaPlFkJDuxfpfxuky3aPzQpxDilU4cf1CfIDua8qivZKQ4QQOd1EaBMXPIpLI1ZTQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.515.0", + "@aws-sdk/client-sso": "3.515.0", + "@aws-sdk/client-sts": "3.515.0", + "@aws-sdk/credential-provider-cognito-identity": "3.515.0", + "@aws-sdk/credential-provider-env": "3.515.0", + "@aws-sdk/credential-provider-http": "3.515.0", + "@aws-sdk/credential-provider-ini": "3.515.0", + "@aws-sdk/credential-provider-node": "3.515.0", + "@aws-sdk/credential-provider-process": "3.515.0", + "@aws-sdk/credential-provider-sso": "3.515.0", + "@aws-sdk/credential-provider-web-identity": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.515.0.tgz", + "integrity": "sha512-I1MwWPzdRKM1luvdDdjdGsDjNVPhj9zaIytEchjTY40NcKOg+p2evLD2y69ozzg8pyXK63r8DdvDGOo9QPuh0A==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.515.0.tgz", + "integrity": "sha512-qXomJzg2m/5seQOxHi/yOXOKfSjwrrJSmEmfwJKJyQgdMbBcjz3Cz0H/1LyC6c5hHm6a/SZgSTzDAbAoUmyL+Q==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.515.0.tgz", + "integrity": "sha512-dokHLbTV3IHRIBrw9mGoxcNTnQsjlm7TpkJhPdGT9T4Mq399EyQo51u6IsVMm07RXLl2Zw7u+u9p+qWBFzmFRA==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.515.0.tgz", + "integrity": "sha512-nOqZjGA/GkjuJ5fUshec9Fv6HFd7ovOTxMJbw3MfAhqXuVZ6dKF41lpVJ4imNsgyFt3shUg9WDY8zGFjlYMB3g==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@aws-sdk/util-endpoints": "3.515.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.515.0.tgz", + "integrity": "sha512-RIRx9loxMgEAc/r1wPfnfShOuzn4RBi8pPPv6/jhhITEeMnJe6enAh2k5y9DdiVDDgCWZgVFSv0YkAIfzAFsnQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", + "@smithy/util-config-provider": "^2.2.1", + "@smithy/util-middleware": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.515.0.tgz", + "integrity": "sha512-MQuf04rIcTXqwDzmyHSpFPF1fKEzRl64oXtCRUF3ddxTdK6wxXkePfK6wNCuL+GEbEcJAoCtIGIRpzGPJvQjHA==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/client-sso-oidc": "3.515.0", + "@aws-sdk/types": "3.515.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.515.0.tgz", + "integrity": "sha512-B3gUpiMlpT6ERaLvZZ61D0RyrQPsFYDkCncLPVkZOKkCOoFU46zi1o6T5JcYiz8vkx1q9RGloQ5exh79s5pU/w==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.515.0.tgz", + "integrity": "sha512-UJi+jdwcGFV/F7d3+e2aQn5yZOVpDiAgfgNhPnEtgV0WozJ5/ZUeZBgWvSc/K415N4A4D/9cbBc7+I+35qzcDQ==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/types": "^2.9.1", + "@smithy/util-endpoints": "^1.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.495.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.495.0.tgz", + "integrity": "sha512-MfaPXT0kLX2tQaR90saBT9fWQq2DHqSSJRzW+MZWsmF+y5LGCOhO22ac/2o6TKSQm7h0HRc2GaADqYYYor62yg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.515.0.tgz", + "integrity": "sha512-pTWQb0JCafTmLHLDv3Qqs/nAAJghcPdGQIBpsCStb0YEzg3At/dOi2AIQ683yYnXmeOxLXJDzmlsovfVObJScw==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/types": "^2.9.1", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.515.0.tgz", + "integrity": "sha512-A/KJ+/HTohHyVXLH+t/bO0Z2mPrQgELbQO8tX+B2nElo8uklj70r5cT7F8ETsI9oOy+HDVpiL5/v45ZgpUOiPg==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.515.0", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz", + "integrity": "sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==", + "dev": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.1.tgz", + "integrity": "sha512-1+qdrUqLhaALYL0iOcN43EP6yAXXQ2wWZ6taf4S2pNGowmOc5gx+iMQv+E42JizNJjB0+gEadOXeV1Bf7JWL1Q==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.1.tgz", + "integrity": "sha512-lxfLDpZm+AWAHPFZps5JfDoO9Ux1764fOgvRUBpHIO8HWHcSN1dkgsago1qLRVgm1BZ8RCm8cgv99QvtaOWIhw==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", + "@smithy/util-config-provider": "^2.2.1", + "@smithy/util-middleware": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.2.tgz", + "integrity": "sha512-tYDmTp0f2TZVE18jAOH1PnmkngLQ+dOGUlMd1u67s87ieueNeyqhja6z/Z4MxhybEiXKOWFOmGjfTZWFxljwJw==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-middleware": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.1.tgz", + "integrity": "sha512-7XHjZUxmZYnONheVQL7j5zvZXga+EWNgwEAP6OPZTi7l8J4JTeNh9aIOfE5fKHZ/ee2IeNOh54ZrSna+Vc6TFA==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/node-config-provider": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.1.tgz", + "integrity": "sha512-E8KYBxBIuU4c+zrpR22VsVrOPoEDzk35bQR3E+xm4k6Pa6JqzkDOdMyf9Atac5GPNKHJBdVaQ4JtjdWX2rl/nw==", + "dev": true, + "optional": true, + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.9.1", + "@smithy/util-hex-encoding": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.1.tgz", + "integrity": "sha512-VYGLinPsFqH68lxfRhjQaSkjXM7JysUOJDTNjHBuN/ykyRb2f1gyavN9+VhhPTWCy32L4yZ2fdhpCs/nStEicg==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/protocol-http": "^3.1.1", + "@smithy/querystring-builder": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-base64": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.1.tgz", + "integrity": "sha512-Qhoq0N8f2OtCnvUpCf+g1vSyhYQrZjhSwvJ9qvR8BUGOtTXiyv2x1OD2e6jVGmlpC4E4ax1USHoyGfV9JFsACg==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "@smithy/util-buffer-from": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.1.tgz", + "integrity": "sha512-7WTgnKw+VPg8fxu2v9AlNOQ5yaz6RA54zOVB4f6vQuR0xFKd+RzlCpt0WidYTsye7F+FYDIaS/RnJW4pxjNInw==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.1.1.tgz", + "integrity": "sha512-xozSQrcUinPpNPNPds4S7z/FakDTh1MZWtRP/2vQtYB/u3HYrX2UXuZs+VhaKBd6Vc7g2XPr2ZtwGBNDN6fNKQ==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.1.tgz", + "integrity": "sha512-rSr9ezUl9qMgiJR0UVtVOGEZElMdGFyl8FzWEF5iEKTlcWxGr2wTqGfDwtH3LAB7h+FPkxqv4ZU4cpuCN9Kf/g==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.1.tgz", + "integrity": "sha512-XPZTb1E2Oav60Ven3n2PFx+rX9EDsU/jSTA8VDamt7FXks67ekjPY/XrmmPDQaFJOTUHJNKjd8+kZxVO5Ael4Q==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/middleware-serde": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-middleware": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.1.tgz", + "integrity": "sha512-eMIHOBTXro6JZ+WWzZWd/8fS8ht5nS5KDQjzhNMHNRcG5FkNTqcKpYhw7TETMYzbLfhO5FYghHy1vqDWM4FLDA==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/node-config-provider": "^2.2.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/service-error-classification": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.1.tgz", + "integrity": "sha512-D8Gq0aQBeE1pxf3cjWVkRr2W54t+cdM2zx78tNrVhqrDykRA7asq8yVJij1u5NDtKzKqzBSPYh7iW0svUKg76g==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.1.tgz", + "integrity": "sha512-KPJhRlhsl8CjgGXK/DoDcrFGfAqoqvuwlbxy+uOO4g2Azn1dhH+GVfC3RAp+6PoL5PWPb+vt6Z23FP+Mr6qeCw==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.1.tgz", + "integrity": "sha512-epzK3x1xNxA9oJgHQ5nz+2j6DsJKdHfieb+YgJ7ATWxzNcB7Hc+Uya2TUck5MicOPhDV8HZImND7ZOecVr+OWg==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.3.1.tgz", + "integrity": "sha512-gLA8qK2nL9J0Rk/WEZSvgin4AppvuCYRYg61dcUo/uKxvMZsMInL5I5ZdJTogOvdfVug3N2dgI5ffcUfS4S9PA==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/abort-controller": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/querystring-builder": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.1.tgz", + "integrity": "sha512-FX7JhhD/o5HwSwg6GLK9zxrMUrGnb3PzNBrcthqHKBc3dH0UfgEAU24xnJ8F0uow5mj17UeBEOI6o3CF2k7Mhw==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.1.1.tgz", + "integrity": "sha512-6ZRTSsaXuSL9++qEwH851hJjUA0OgXdQFCs+VDw4tGH256jQ3TjYY/i34N4vd24RV3nrjNsgd1yhb57uMoKbzQ==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.1.tgz", + "integrity": "sha512-C/ko/CeEa8jdYE4gt6nHO5XDrlSJ3vdCG0ZAc6nD5ZIE7LBp0jCx4qoqp7eoutBu7VrGMXERSRoPqwi1WjCPbg==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "@smithy/util-uri-escape": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.1.tgz", + "integrity": "sha512-H4+6jKGVhG1W4CIxfBaSsbm98lOO88tpDWmZLgkJpt8Zkk/+uG0FmmqMuCAc3HNM2ZDV+JbErxr0l5BcuIf/XQ==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.1.tgz", + "integrity": "sha512-txEdZxPUgM1PwGvDvHzqhXisrc5LlRWYCf2yyHfvITWioAKat7srQvpjMAvgzf0t6t7j8yHrryXU9xt7RZqFpw==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.1.tgz", + "integrity": "sha512-2E2kh24igmIznHLB6H05Na4OgIEilRu0oQpYXo3LCNRrawHAcfDKq9004zJs+sAMt2X5AbY87CUCJ7IpqpSgdw==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.1.tgz", + "integrity": "sha512-Hb7xub0NHuvvQD3YwDSdanBmYukoEkhqBjqoxo+bSdC0ryV9cTfgmNjuAQhTPYB6yeU7hTR+sPRiFMlxqv6kmg==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/eventstream-codec": "^2.1.1", + "@smithy/is-array-buffer": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-uri-escape": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.3.1.tgz", + "integrity": "sha512-YsTdU8xVD64r2pLEwmltrNvZV6XIAC50LN6ivDopdt+YiF/jGH6PY9zUOu0CXD/d8GMB8gbhnpPsdrjAXHS9QA==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-stream": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.9.1.tgz", + "integrity": "sha512-vjXlKNXyprDYDuJ7UW5iobdmyDm6g8dDG+BFUncAg/3XJaN45Gy5RWWWUVgrzIK7S4R1KWgIX5LeJcfvSI24bw==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.1.tgz", + "integrity": "sha512-qC9Bv8f/vvFIEkHsiNrUKYNl8uKQnn4BdhXl7VzQRP774AwIjiSMMwkbT+L7Fk8W8rzYVifzJNYxv1HwvfBo3Q==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/querystring-parser": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.1.1.tgz", + "integrity": "sha512-UfHVpY7qfF/MrgndI5PexSKVTxSZIdz9InghTFa49QOvuu9I52zLPLUHXvHpNuMb1iD2vmc6R+zbv/bdMipR/g==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.1.1.tgz", + "integrity": "sha512-ekOGBLvs1VS2d1zM2ER4JEeBWAvIOUKeaFch29UjjJsxmZ/f0L3K3x0dEETgh3Q9bkZNHgT+rkdl/J/VUqSRag==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.2.1.tgz", + "integrity": "sha512-/ggJG+ta3IDtpNVq4ktmEUtOkH1LW64RHB5B0hcr5ZaWBmo96UX2cIOVbjCqqDickTXqBWZ4ZO0APuaPrD7Abg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.1.1.tgz", + "integrity": "sha512-clhNjbyfqIv9Md2Mg6FffGVrJxw7bgK7s3Iax36xnfVj6cg0fUG7I4RH0XgXJF8bxi+saY5HR21g2UPKSxVCXg==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.2.1.tgz", + "integrity": "sha512-50VL/tx9oYYcjJn/qKqNy7sCtpD0+s8XEBamIFo4mFFTclKMNp+rsnymD796uybjiIquB7VCB/DeafduL0y2kw==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.1.tgz", + "integrity": "sha512-lqLz/9aWRO6mosnXkArtRuQqqZBhNpgI65YDpww4rVQBuUT7qzKbDLG5AmnQTCiU4rOquaZO/Kt0J7q9Uic7MA==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/property-provider": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.0.tgz", + "integrity": "sha512-iFJp/N4EtkanFpBUtSrrIbtOIBf69KNuve03ic1afhJ9/korDxdM0c6cCH4Ehj/smI9pDCfVv+bqT3xZjF2WaA==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/config-resolver": "^2.1.1", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.1.tgz", + "integrity": "sha512-sI4d9rjoaekSGEtq3xSb2nMjHMx8QXcz2cexnVyRWsy4yQ9z3kbDpX+7fN0jnbdOp0b3KSTZJZ2Yb92JWSanLw==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.1.1.tgz", + "integrity": "sha512-3UNdP2pkYUUBGEXzQI9ODTDK+Tcu1BlCyDBaRHwyxhA+8xLP8agEKQq4MGmpjqb4VQAjq9TwlCQX0kP6XDKYLg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.1.tgz", + "integrity": "sha512-mKNrk8oz5zqkNcbcgAAepeJbmfUW6ogrT2Z2gDbIUzVzNAHKJQTYmH9jcy0jbWb+m7ubrvXKb6uMjkSgAqqsFA==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.1.tgz", + "integrity": "sha512-Mg+xxWPTeSPrthpC5WAamJ6PW4Kbo01Fm7lWM1jmGRvmrRdsd3192Gz2fBXAMURyXpaNxyZf6Hr/nQ4q70oVEA==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/service-error-classification": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.1.tgz", + "integrity": "sha512-J7SMIpUYvU4DQN55KmBtvaMc7NM3CZ2iWICdcgaovtLzseVhAqFRYqloT3mh0esrFw+3VEK6nQFteFsTqZSECQ==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-buffer-from": "^2.1.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.1.1.tgz", + "integrity": "sha512-saVzI1h6iRBUVSqtnlOnc9ssU09ypo7n+shdQ8hBTZno/9rZ3AuRYvoHInV57VF7Qn7B+pFJG7qTzFiHxWlWBw==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.1.1.tgz", + "integrity": "sha512-BqTpzYEcUMDwAKr7/mVRUtHDhs6ZoXDi9NypMvMfOr/+u1NW7JgqodPDECiiLboEm6bobcPcECxzjtQh865e9A==", + "dev": true, + "optional": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/dot-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/dot-object/-/dot-object-2.1.6.tgz", + "integrity": "sha512-G1e4SNPOuO72ZXv7wz/W2x29CzQtpxko3G9hBiHqGg/AvFIKoArCs2nbc/WPXnnUkO+1dmvX9WQCyj5gIlAzZg==", + "dev": true + }, + "node_modules/@types/jquery": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", + "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==", + "dev": true, + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", + "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/meteor": { + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-2.9.8.tgz", + "integrity": "sha512-p96s1lMqtwt0hz50yokQJA+V9BQzNMLJfahwlbbfXKwbI/OMNCKhRfnxiiNROXAF080zctJlEcpjmv0YG7ztkA==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/jquery": "*", + "@types/node": "*", + "@types/nodemailer": "*", + "@types/react": "*", + "@types/underscore": "*", + "mongodb": "^4.3.1" + } + }, + "node_modules/@types/meteor/node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/meteor/node_modules/bson": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", + "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", + "dev": true, + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/meteor/node_modules/mongodb": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.17.2.tgz", + "integrity": "sha512-mLV7SEiov2LHleRJPMPrK2PMyhXFZt2UQLC4VD4pnth3jMjYKHhtqfwwkkvS/NXuo/Fp3vbhaNcXrIDaLRb9Tg==", + "dev": true, + "dependencies": { + "bson": "^4.7.2", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=12.9.0" + }, + "optionalDependencies": { + "@aws-sdk/credential-providers": "^3.186.0", + "@mongodb-js/saslprep": "^1.1.0" + } + }, + "node_modules/@types/meteor/node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/@types/meteor/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/meteor/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz", + "integrity": "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", + "integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.56", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.56.tgz", + "integrity": "sha512-NpwHDMkS/EFZF2dONFQHgkPRwhvgq/OAvIaGQzxGSBmaeR++kTg6njr15Vatz0/2VcCEwJQFi6Jf4Q0qBu0rLA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", + "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, + "node_modules/@types/underscore": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.15.tgz", + "integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==", + "dev": true + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dev": true, + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", + "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/type-utils": "7.0.2", + "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", + "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", + "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", + "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/utils": "7.0.2", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", + "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", + "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", + "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", + "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.0.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { + "dev": true, + "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" } }, - "array-union": { + "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "^1.0.1" + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "dev": true, + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz", + "integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==", + "dev": true, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "dev": true, + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "requires": { - "safer-buffer": "~2.1.0" + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "brace-expansion": { + "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { + "dev": true, + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" - }, - "chromedriver": { - "version": "2.36.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-2.36.0.tgz", - "integrity": "sha512-Lq2HrigCJ4RVdIdCmchenv1rVrejNSJ7EUCQojycQo12ww3FedQx4nb+GgTdqMhjbOMTqq5+ziaiZlrEN2z1gQ==", - "requires": { - "del": "^3.0.0", - "extract-zip": "^1.6.5", - "kew": "^0.7.0", - "mkdirp": "^0.5.1", - "request": "^2.83.0" - } - }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" } }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "requires": { - "type-detect": "^4.0.0" + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" } }, - "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", - "requires": { - "globby": "^6.1.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "p-map": "^1.1.1", - "pify": "^3.0.0", - "rimraf": "^2.2.8" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "requires": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "requires": { - "pend": "~1.2.0" + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" } }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + "node_modules/mongodb": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.3.0.tgz", + "integrity": "sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==", + "dev": true, + "dependencies": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^6.2.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" + "node_modules/mongodb-connection-string-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", + "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", + "dev": true, + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" } }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "requires": { - "is-path-inside": "^1.0.0" + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" } }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "requires": { - "path-is-inside": "^1.0.1" + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, - "jszip": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.9.0.tgz", - "integrity": "sha512-Vb3SMfASUN1EKrFzv5A5+lTaZnzLzT5E6A9zyT7WFqMSfhT2Z7iS5FgSOjx2Olm3MDj8OqKj6GHyP2kMt1Ir6w==", - "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" - } - }, - "kew": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", - "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=" - }, - "lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "requires": { - "immediate": "~3.0.5" + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" } }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" } }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "requires": { - "lodash._reinterpolate": "^3.0.0" + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" } }, - "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "requires": { - "get-func-name": "^2.0.0" + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "message-box": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/message-box/-/message-box-0.2.7.tgz", - "integrity": "sha512-C4ccA5nHb58kTS+pLrgF/JWtr7fAIkHxRDceH7tdy5fMA783nUfbYwZ7H2XLvSeYfcnWIYCig5dWW+icK9X/Ag==", - "requires": { - "lodash.template": "^4.5.0" + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" } }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" } }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "requires": { - "minimist": "^1.2.6" + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" } }, - "mongo-object": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/mongo-object/-/mongo-object-0.1.4.tgz", - "integrity": "sha512-QtYk0gupWEn2+iB+DDRt1L+WbcNYvJRaHdih/dcqthOa1DbnREUGSs2WGcW478GNYpElflo/yybZXu0sTiRXHg==" + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "ms": { + "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "node_modules/socks": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.3.tgz", + "integrity": "sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dev": true, + "dependencies": { + "memory-pager": "^1.0.2" + } }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true, + "optional": true }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" + "node_modules/ts-api-utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", + "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true, + "optional": true }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "selenium-webdriver": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", - "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", - "requires": { - "jszip": "^3.1.3", - "rimraf": "^2.5.4", - "tmp": "0.0.30", - "xml2js": "^0.4.17" - } - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" - }, - "simpl-schema": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/simpl-schema/-/simpl-schema-1.12.0.tgz", - "integrity": "sha512-lzXC3L8jJbPhNXGR3cjlyIauqqrC5WUJS4O34Ym/wLIvb8K3ZieK+1OfTzs4mBpDc3Y8u53gQFAr1X37DmTcEg==", - "requires": { - "clone": "^2.1.2", - "message-box": "^0.2.7", - "mongo-object": "^0.1.4" - } - }, - "sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "tmp": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", - "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", - "requires": { - "os-tmpdir": "~1.0.1" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, - "uri-js": { + "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { + "dev": true, + "dependencies": { "punycode": "^2.1.0" } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - } + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dev": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } } } diff --git a/package-types.json b/package-types.json new file mode 100644 index 00000000..957d4d05 --- /dev/null +++ b/package-types.json @@ -0,0 +1,3 @@ +{ + "typesEntry": "@types/index.d.ts" +} \ No newline at end of file diff --git a/package.js b/package.js index 4be099c7..1df2525c 100755 --- a/package.js +++ b/package.js @@ -1,106 +1,127 @@ Package.describe({ - name: "cultofcoders:grapher", - version: "1.5.0", + name: 'cultofcoders:grapher', + version: '2.0.0-beta.1', // Brief, one-line summary of the package. - summary: "Grapher is a data fetching layer on top of Meteor", + summary: 'Grapher is a data fetching layer on top of Meteor', // URL to the Git repository containing the source code for this package. - git: "https://github.com/cult-of-coders/grapher", + git: 'https://github.com/cult-of-coders/grapher', // By default, Meteor will default to using README.md for documentation. // To avoid submitting documentation, set this field to null. - documentation: "README.md" + documentation: 'README.md', }); const npmPackages = { - "sift": "15.0.0", - "dot-object": "1.9.0", - "lodash.clonedeep": "4.5.0", - "deep-extend": "0.6.0", -} + sift: '15.0.0', + 'dot-object': '1.9.0', + 'lodash.clonedeep': '4.5.0', + 'deep-extend': '0.6.0', +}; Package.onUse(function (api) { Npm.depends(npmPackages); - api.versionsFrom(["2.3.1", "2.6.1", "2.7.3", "2.8.1", "2.9.1", "3.0-beta.4"]); + api.versionsFrom(['3.0.1']); + + api.addFiles('lib/scoping/client.js', 'client'); + api.addFiles('lib/scoping/server.js', 'server'); var packages = [ - "ecmascript", - "underscore", - "promise", - "check", - "reactive-var", - "mongo", - "matb33:collection-hooks@1.3.1", - "reywood:publish-composite@1.8.8", - "dburles:mongo-collection-instances@0.4.0", - "peerlibrary:subscription-scope@0.5.0", - "herteby:denormalize@0.6.7" + 'ecmascript', + 'underscore', + 'promise', + 'check', + 'reactive-var', + 'zodern:types@1.0.13', + 'mongo', + + // https://github.com/Meteor-Community-Packages/meteor-collection-hooks/ + 'matb33:collection-hooks@2.0.0', + + // https://github.com/Meteor-Community-Packages/meteor-publish-composite + 'reywood:publish-composite@1.8.9', + + // https://github.com/Meteor-Community-Packages/mongo-collection-instances + 'dburles:mongo-collection-instances@1.0.0', + + // Note: seems to be not working. Getting weird conflict that cultofcoders:grapher@1.5.0 depends on version 0.1.0 + // https://github.com/peerlibrary/meteor-subscription-scope + // 'peerlibrary:subscription-scope@0.5.0', + + // https://github.com/Meteor-Community-Packages/denormalize/ + 'herteby:denormalize@0.7.0-beta.1', ]; api.use(packages); - api.mainModule("main.client.js", "client"); - api.mainModule("main.server.js", "server"); + api.mainModule('main.client.js', 'client'); + api.mainModule('main.server.js', 'server'); }); Package.onTest(function (api) { - api.use("cultofcoders:grapher"); + api.use('cultofcoders:grapher'); + + // api.addFiles('lib/scoping/client.js', 'client'); + // api.addFiles('lib/scoping/server.js', 'server'); Npm.depends({ ...npmPackages, - chai: "4.3.4" + chai: '4.3.4', }); var packages = [ - "random", - "ecmascript", - "underscore", - "matb33:collection-hooks@1.3.1", - "reywood:publish-composite@1.8.7", - "dburles:mongo-collection-instances@0.4.0", - "peerlibrary:subscription-scope@0.5.0", - "herteby:denormalize@0.6.7", - "mongo" + 'random', + 'ecmascript', + 'underscore', + 'matb33:collection-hooks@2.0.0', + 'reywood:publish-composite@1.8.12', + 'dburles:mongo-collection-instances@1.0.0', + // 'peerlibrary:subscription-scope@0.5.0', + 'herteby:denormalize@0.7.0-beta.1', + 'mongo', ]; api.use(packages); - api.use("tracker"); + api.use('tracker'); - api.use(["meteortesting:mocha"]); + api.use(['meteortesting:mocha']); // LINKS - api.addFiles("lib/links/tests/main.js", "server"); - api.addFiles("lib/links/tests/client.test.js", "client"); + api.addFiles('lib/links/tests/main.js', 'server'); + api.addFiles('lib/links/tests/client.test.js', 'client'); // EXPOSURE - api.addFiles("lib/exposure/testing/server.js", "server"); - api.addFiles("lib/exposure/testing/client.js", "client"); + api.addFiles('lib/exposure/testing/server.js', 'server'); + api.addFiles('lib/exposure/testing/client.js', 'client'); // QUERY - api.addFiles("lib/query/testing/bootstrap/index.js"); + api.addFiles('lib/query/testing/bootstrap/index.js'); // When you play with tests you should comment this to make tests go faster. - api.addFiles("lib/query/testing/bootstrap/fixtures.js", "server"); + api.addFiles('lib/query/testing/bootstrap/fixtures.js', 'server'); - api.addFiles("lib/query/testing/server.test.js", "server"); - api.addFiles("lib/query/testing/client.test.js", "client"); + api.addFiles('lib/query/testing/server.test.js', 'server'); + api.addFiles('lib/query/testing/client.test.js', 'client'); // NAMED QUERY - api.addFiles("lib/namedQuery/testing/bootstrap/both.js"); - api.addFiles("lib/namedQuery/testing/bootstrap/client.js", "client"); - api.addFiles("lib/namedQuery/testing/bootstrap/server.js", "server"); + api.addFiles('lib/namedQuery/testing/bootstrap/both.js'); + api.addFiles('lib/namedQuery/testing/bootstrap/client.js', 'client'); + api.addFiles('lib/namedQuery/testing/bootstrap/server.js', 'server'); // REACTIVE COUNTS - api.addFiles("lib/query/counts/testing/server.test.js", "server"); - api.addFiles("lib/query/counts/testing/client.test.js", "client"); + api.addFiles('lib/query/counts/testing/server.test.js', 'server'); + api.addFiles('lib/query/counts/testing/client.test.js', 'client'); // NAMED QUERIES - api.addFiles("lib/namedQuery/testing/server.test.js", "server"); - api.addFiles("lib/namedQuery/testing/client.test.js", "client"); + api.addFiles('lib/namedQuery/testing/server.test.js', 'server'); + api.addFiles('lib/namedQuery/testing/client.test.js', 'client'); // hypernova - api.addFiles("lib/query/hypernova/testing/assembler.test.js", "server"); - api.addFiles("lib/query/hypernova/testing/processVirtualNode.test.js", "server"); + api.addFiles('lib/query/hypernova/testing/assembler.test.js', 'server'); + api.addFiles( + 'lib/query/hypernova/testing/processVirtualNode.test.js', + 'server', + ); // GRAPHQL - api.addFiles("lib/graphql/testing/index.js", "server"); + api.addFiles('lib/graphql/testing/index.js', 'server'); }); diff --git a/package.json b/package.json new file mode 100644 index 00000000..9f73fdd2 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "scripts": { + "lint": "eslint ." + }, + "devDependencies": { + "@types/chai": "^4.3.20", + "@types/dot-object": "^2.1.6", + "@types/lodash.clonedeep": "^4.5.9", + "@types/meteor": "^2.9.8", + "@types/mocha": "^10.0.10", + "@types/node": "^20.11.30", + "@typescript-eslint/eslint-plugin": "^7.0.2", + "@typescript-eslint/parser": "^7.0.2", + "eslint": "^8.56.0", + "mongodb": "^6.3.0", + "typescript": "^5.7.3" + }, + "volta": { + "node": "20.16.0" + } +} diff --git a/test.ts b/test.ts new file mode 100644 index 00000000..a7b125c3 --- /dev/null +++ b/test.ts @@ -0,0 +1,3 @@ +export function getConfig(linkConfig: Grapher.LinkConfig): string { + return 1; +} diff --git a/tsconfig.json b/tsconfig.json index fd9a9a53..f646d1b3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,71 +1,113 @@ { "compilerOptions": { - /* Basic Options */ - "target": "ES2016" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, - "module": "esNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, - "lib": [ - "es2015", - "es2016", - "es2017", - "dom", - "scripthost", - "dom.iterable" - ] /* Specify library files to be included in the compilation. */, - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, - // "declaration": true /* Generates corresponding '.d.ts' file. */, - "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, - "sourceMap": true /* Generates corresponding '.map' file. */, - // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./dist" /* Redirect output structure to the directory. */, - "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, - // "composite": true, /* Enable project compilation */ - "removeComments": true /* Do not emit comments to output. */, - // "noEmit": true, /* Do not emit outputs. */ - "importHelpers": true /* Import emit helpers from 'tslib'. */, - "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, - // "strictNullChecks": true /* Enable strict null checks. */, - "strictFunctionTypes": true /* Enable strict checking of function types. */, - "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, - "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, - "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "es2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + "typeRoots": [ + "./node_modules/@types", + "./@types" + ] /* Specify multiple folders that act like './node_modules/@types'. */, + // "types": [] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* Additional Checks */ - "noUnusedLocals": true /* Report errors on unused locals. */, - "noUnusedParameters": true /* Report errors on unused parameters. */, - "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, - "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Module Resolution Options */ - "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, - "baseUrl": "./lib" /* Base directory to resolve non-absolute module names. */, - "paths": {} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */, - // "rootDirs": [ - // "/client" - // ] /* List of root folders whose combined content represents the structure of the project at runtime. */, - // "typeRoots": [], /* List of folders to include type definitions from. */ - "types": [ - "./index.d.ts", - "@types/*.d.ts" - ] /* Type declaration files to be included in compilation. */, - "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + "noEmit": true /* Disable emitting files from a compilation. */, + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Experimental Options */ - // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, - // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ - } + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "exclude": ["test"] }