diff --git a/.circleci/config.yml b/.circleci/config.yml index d1447afdc..37ac47b94 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - pocket: pocket/circleci-orbs@2.1.1 + pocket: pocket/circleci-orbs@2.2.2 backstage-entity-validator: roadiehq/backstage-entity-validator@0.4.2 # Workflow shortcuts @@ -47,7 +47,7 @@ only_dev: &only_dev jobs: build: docker: - - image: cimg/node:18@sha256:45826c38fb365c2848f3f595443b3086b1df4256d659b380bb9b666141eceadd + - image: cimg/node:18.20 steps: - checkout # Define the working directory for this job @@ -89,7 +89,7 @@ jobs: description: Run integration tests against external services, e.g. Snowplow docker: # The application - - image: cimg/node:18@sha256:45826c38fb365c2848f3f595443b3086b1df4256d659b380bb9b666141eceadd + - image: cimg/node:18.20 auth: username: $DOCKERHUB_USERNAME password: $DOCKERHUB_PASSWORD @@ -127,7 +127,7 @@ jobs: test_specs: description: Run spec tests docker: - - image: cimg/node:18@sha256:45826c38fb365c2848f3f595443b3086b1df4256d659b380bb9b666141eceadd + - image: cimg/node:18.20 auth: username: $DOCKERHUB_USERNAME password: $DOCKERHUB_PASSWORD diff --git a/.nvmrc b/.nvmrc index 4a58985bb..0305213fe 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.18 +18.20 diff --git a/Dockerfile b/Dockerfile index d9a3617f9..9293ef256 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-slim@sha256:0a621cdd7d66ad8976f4246ab0661e3b1dd0d397c1dd784ea01bf740bd1c2522 +FROM node:18@sha256:332838eb5ed61f24f5f68e3e465453d82ea8cd8870d9875325f20706880ae9fc WORKDIR /usr/src/app diff --git a/package-lock.json b/package-lock.json index d09e89867..9cb6dac08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.14", "@types/jest": "29.5.12", - "@types/node": "^18.11.9", + "@types/node": "^18.19.31", "fetch-mock-jest": "1.5.1", "husky": "8.0.1", "jest": "29.7.0", @@ -45,7 +45,7 @@ "openapi-typescript": "^6.0.3", "supertest": "6.3.3", "ts-jest": "29.1.2", - "ts-node": "^10.9.1" + "ts-node": "^10.9.2" }, "engines": { "node": "=18" @@ -4636,9 +4636,12 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.19.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", + "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -17449,9 +17452,9 @@ "dev": true }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -17631,6 +17634,11 @@ "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", @@ -21698,9 +21706,12 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.19.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", + "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", + "requires": { + "undici-types": "~5.26.4" + } }, "@types/normalize-package-data": { "version": "2.4.1", @@ -30950,9 +30961,9 @@ "dev": true }, "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -31056,6 +31067,11 @@ "busboy": "^1.6.0" } }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", diff --git a/package.json b/package.json index 1397ebd45..4eb8d0399 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "dist/main.js", "scripts": { "build": "rm -rf dist && tsc", - "codegen": "npm run codegen:graphql-types && npm run codegen:openapi-types", + "codegen": "npm run codegen:graphql-types", "codegen:graphql-types": "graphql-codegen", "codegen:openapi-types": "npm run lint-openapi && ts-node-esm -P scripts/tsconfig.openapi-typescript.json scripts/generateOpenAPITypes.mts", "docs": "redocly preview-docs openapi.yml", @@ -62,7 +62,7 @@ "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.14", "@types/jest": "29.5.12", - "@types/node": "^18.11.9", + "@types/node": "^18.19.31", "fetch-mock-jest": "1.5.1", "husky": "8.0.1", "jest": "29.7.0", @@ -71,6 +71,6 @@ "openapi-typescript": "^6.0.3", "supertest": "6.3.3", "ts-jest": "29.1.2", - "ts-node": "^10.9.1" + "ts-node": "^10.9.2" } } diff --git a/src/generated/graphql/types.ts b/src/generated/graphql/types.ts index 70ca02e22..7622d4538 100644 --- a/src/generated/graphql/types.ts +++ b/src/generated/graphql/types.ts @@ -796,9 +796,26 @@ export type ItemHighlights = { url?: Maybe>>; }; +export type ItemNotFound = { + __typename?: 'ItemNotFound'; + message?: Maybe; +}; + /** Union type for items that may or may not be processed */ export type ItemResult = Item | PendingItem; +export type ItemSummary = { + __typename?: 'ItemSummary'; + authors?: Maybe>; + datePublished?: Maybe; + domain?: Maybe; + excerpt?: Maybe; + image?: Maybe; + item?: Maybe; + title?: Maybe; + url: Scalars['Url']; +}; + /** A label used to mark and categorize an Entity (e.g. Collection). */ export type Label = { __typename?: 'Label'; @@ -1709,6 +1726,31 @@ export enum PocketSaveStatus { Unread = 'UNREAD' } +export enum PremiumFeature { + /** Feature where you get an ad-free experience */ + AdFree = 'AD_FREE', + /** Feature where you can highlight articles */ + Annotations = 'ANNOTATIONS', + /** Feature where pocket saves permanent copies of all your saves */ + PermanentLibrary = 'PERMANENT_LIBRARY', + /** Feature where pocket's search is enhanced */ + PremiumSearch = 'PREMIUM_SEARCH', + /** Feature where pocket suggests tags */ + SuggestedTags = 'SUGGESTED_TAGS' +} + +export enum PremiumStatus { + /** + * User has premium and its active + * NOTE: User will still show as active if they turn off auto-renew or have otherwise canceled but the expiration date hasn't hit yet + */ + Active = 'ACTIVE', + /** User has had premium, but it is expired */ + Expired = 'EXPIRED', + /** User has never had premium */ + Never = 'NEVER' +} + /** The publisher that the curation team set for the syndicated article */ export type Publisher = { __typename?: 'Publisher'; @@ -1811,6 +1853,14 @@ export type Query = { listTopics: Array; /** Get a slate of ranked recommendations for the Firefox New Tab. Currently supports the Italy, France, and Spain markets. */ newTabSlate: CorpusSlate; + /** + * Resolve Reader View links which might point to SavedItems that do not + * exist, aren't in the Pocket User's list, or are requested by a logged-out + * user (or user without a Pocket Account). + * Fetches data to create an interstitial page/modal so the visitor can click + * through to the shared site. + */ + readerSlug: ReaderViewResult; /** List all topics that the user can express a preference for. */ recommendationPreferenceTopics: Array; scheduledSurface: ScheduledSurface; @@ -1953,6 +2003,15 @@ export type QueryNewTabSlateArgs = { }; +/** + * Default root level query type. All authorization checks are done in these queries. + * TODO: These belong in a seperate User Service that provides a User object (the user settings will probably exist there too) + */ +export type QueryReaderSlugArgs = { + slug: Scalars['ID']; +}; + + /** * Default root level query type. All authorization checks are done in these queries. * TODO: These belong in a seperate User Service that provides a User object (the user settings will probably exist there too) @@ -2007,6 +2066,24 @@ export type QueryUnleashAssignmentsArgs = { context: UnleashContext; }; +export type ReaderFallback = ItemNotFound | ReaderInterstitial; + +export type ReaderInterstitial = { + __typename?: 'ReaderInterstitial'; + itemCard?: Maybe; +}; + +export type ReaderViewResult = { + __typename?: 'ReaderViewResult'; + fallbackPage?: Maybe; + /** + * The SavedItem referenced by this reader view slug, if it + * is in the Pocket User's list. + */ + savedItem?: Maybe; + slug: Scalars['ID']; +}; + export type RecItUserProfile = { userModels: Array; }; @@ -3106,6 +3183,8 @@ export type User = { email?: Maybe; /** The users first name */ firstName?: Maybe; + /** User id, provided by the user service. */ + id: Scalars['ID']; /** Indicates if a user is FxA or not */ isFxa?: Maybe; /** The user's premium status */ @@ -3114,6 +3193,10 @@ export type User = { lastName?: Maybe; /** The users first name and last name combined */ name?: Maybe; + /** Premium features that a user has access to */ + premiumFeatures?: Maybe>>; + /** Current premium status of the user */ + premiumStatus?: Maybe; /** Preferences for recommendations that the user has explicitly set. */ recommendationPreferences?: Maybe; /** Get a PocketSave(s) by its id(s) */ @@ -3137,6 +3220,18 @@ export type User = { searchSavedItemsByOffset?: Maybe; /** Get a paginated listing of all a user's Tags */ tags?: Maybe; + /** + * Get all tag names for a user. + * If syncSince is passed, it will only return tags if changes + * to a user's tags have occurred after syncSince. It will return + * all of the user's tags (not just the changes). + * + * Yes, this is bad graphql design. It's serving a specific + * REST API which has unlimited SQL queries, and we do not want to + * make it possible to request every associated SavedItem + * node on a tag object. Just biting the bullet on this one. + */ + tagsList?: Maybe>; /** The public username for the user */ username?: Maybe; }; @@ -3217,6 +3312,12 @@ export type UserTagsArgs = { pagination?: InputMaybe; }; + +/** Resolve by reference the User entity in this graph to provide user data with public lists. */ +export type UserTagsListArgs = { + syncSince?: InputMaybe; +}; + export type UserRecommendationPreferences = { __typename?: 'UserRecommendationPreferences'; /** Topics that the user expressed interest in. */ diff --git a/src/graphql-proxy/recommendations/__mocks__/recommendations.ts b/src/graphql-proxy/recommendations/__mocks__/recommendations.ts index 8ecdcd56a..be1e22354 100644 --- a/src/graphql-proxy/recommendations/__mocks__/recommendations.ts +++ b/src/graphql-proxy/recommendations/__mocks__/recommendations.ts @@ -28,7 +28,6 @@ const fakerLocales = { it: 'it', }; - const fakeRecommendation = (): GraphRecommendation => { const recommendation: GraphRecommendation = { __typename: 'CorpusRecommendation', @@ -51,9 +50,7 @@ const fakeRecommendations = ( ): NewTabRecommendationsQuery['newTabSlate']['recommendations'] => { return Array(count) .fill(0) - .map((value, index) => - fakeRecommendation() - ); + .map((value, index) => fakeRecommendation()); }; const recommendations = async (