From f4ff84eddfb4b1220a180028ca2d76ecce3d4ee7 Mon Sep 17 00:00:00 2001 From: Valery Melou Date: Sat, 25 May 2024 01:27:19 +0100 Subject: [PATCH] feat: add the cms and blog services to query data --- libs/blog/data-access/.eslintrc.json | 36 ++ libs/blog/data-access/README.md | 7 + libs/blog/data-access/jest.config.ts | 22 + libs/blog/data-access/project.json | 34 ++ libs/blog/data-access/src/index.ts | 6 + .../src/lib/article.service.spec.ts | 376 +++++++++++++++++ .../data-access/src/lib/article.service.ts | 41 ++ libs/blog/data-access/src/lib/article.ts | 50 +++ libs/blog/data-access/src/lib/asset.ts | 20 + libs/blog/data-access/src/lib/results.ts | 6 + .../data-access/src/lib/tag.service.spec.ts | 77 ++++ libs/blog/data-access/src/lib/tag.service.ts | 33 ++ libs/blog/data-access/src/lib/tag.ts | 4 + libs/blog/data-access/src/test-setup.ts | 8 + libs/blog/data-access/tsconfig.json | 29 ++ libs/blog/data-access/tsconfig.lib.json | 17 + libs/blog/data-access/tsconfig.spec.json | 16 + libs/cms/contentful/.eslintrc.json | 36 ++ libs/cms/contentful/README.md | 7 + libs/cms/contentful/jest.config.ts | 22 + libs/cms/contentful/project.json | 34 ++ libs/cms/contentful/src/index.ts | 2 + .../src/lib/contentful.service.spec.ts | 396 ++++++++++++++++++ .../contentful/src/lib/contentful.service.ts | 71 ++++ libs/cms/contentful/src/lib/tokens.ts | 9 + libs/cms/contentful/src/test-setup.ts | 8 + libs/cms/contentful/tsconfig.json | 29 ++ libs/cms/contentful/tsconfig.lib.json | 17 + libs/cms/contentful/tsconfig.spec.json | 16 + package.json | 1 + tsconfig.base.json | 4 +- yarn.lock | 107 +++++ 32 files changed, 1540 insertions(+), 1 deletion(-) create mode 100644 libs/blog/data-access/.eslintrc.json create mode 100644 libs/blog/data-access/README.md create mode 100644 libs/blog/data-access/jest.config.ts create mode 100644 libs/blog/data-access/project.json create mode 100644 libs/blog/data-access/src/index.ts create mode 100644 libs/blog/data-access/src/lib/article.service.spec.ts create mode 100644 libs/blog/data-access/src/lib/article.service.ts create mode 100644 libs/blog/data-access/src/lib/article.ts create mode 100644 libs/blog/data-access/src/lib/asset.ts create mode 100644 libs/blog/data-access/src/lib/results.ts create mode 100644 libs/blog/data-access/src/lib/tag.service.spec.ts create mode 100644 libs/blog/data-access/src/lib/tag.service.ts create mode 100644 libs/blog/data-access/src/lib/tag.ts create mode 100644 libs/blog/data-access/src/test-setup.ts create mode 100644 libs/blog/data-access/tsconfig.json create mode 100644 libs/blog/data-access/tsconfig.lib.json create mode 100644 libs/blog/data-access/tsconfig.spec.json create mode 100644 libs/cms/contentful/.eslintrc.json create mode 100644 libs/cms/contentful/README.md create mode 100644 libs/cms/contentful/jest.config.ts create mode 100644 libs/cms/contentful/project.json create mode 100644 libs/cms/contentful/src/index.ts create mode 100644 libs/cms/contentful/src/lib/contentful.service.spec.ts create mode 100644 libs/cms/contentful/src/lib/contentful.service.ts create mode 100644 libs/cms/contentful/src/lib/tokens.ts create mode 100644 libs/cms/contentful/src/test-setup.ts create mode 100644 libs/cms/contentful/tsconfig.json create mode 100644 libs/cms/contentful/tsconfig.lib.json create mode 100644 libs/cms/contentful/tsconfig.spec.json diff --git a/libs/blog/data-access/.eslintrc.json b/libs/blog/data-access/.eslintrc.json new file mode 100644 index 0000000..1f56884 --- /dev/null +++ b/libs/blog/data-access/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "vmelou", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "vmelou", + "style": "kebab-case" + } + ] + }, + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ] + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/blog/data-access/README.md b/libs/blog/data-access/README.md new file mode 100644 index 0000000..68578ac --- /dev/null +++ b/libs/blog/data-access/README.md @@ -0,0 +1,7 @@ +# blog-data-access + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test blog-data-access` to execute the unit tests. diff --git a/libs/blog/data-access/jest.config.ts b/libs/blog/data-access/jest.config.ts new file mode 100644 index 0000000..e0cb591 --- /dev/null +++ b/libs/blog/data-access/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'blog-data-access', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/blog/data-access', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/blog/data-access/project.json b/libs/blog/data-access/project.json new file mode 100644 index 0000000..3f261b0 --- /dev/null +++ b/libs/blog/data-access/project.json @@ -0,0 +1,34 @@ +{ + "name": "blog-data-access", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/data-access/src", + "prefix": "vmelou", + "tags": [], + "projectType": "library", + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/data-access/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "libs/blog/data-access/**/*.ts", + "libs/blog/data-access/**/*.html" + ] + } + } + } +} diff --git a/libs/blog/data-access/src/index.ts b/libs/blog/data-access/src/index.ts new file mode 100644 index 0000000..d5846b1 --- /dev/null +++ b/libs/blog/data-access/src/index.ts @@ -0,0 +1,6 @@ +export * from './lib/article'; +export * from './lib/article.service'; +export * from './lib/asset'; +export * from './lib/results'; +export * from './lib/tag'; +export * from './lib/tag.service'; diff --git a/libs/blog/data-access/src/lib/article.service.spec.ts b/libs/blog/data-access/src/lib/article.service.spec.ts new file mode 100644 index 0000000..a5acc00 --- /dev/null +++ b/libs/blog/data-access/src/lib/article.service.spec.ts @@ -0,0 +1,376 @@ +import { TestBed } from '@angular/core/testing'; + +import { EntryCollection, EntrySkeletonType } from 'contentful'; +import { of } from 'rxjs'; + +import { ContentfulService } from '@valerymelou/cms/contentful'; + +import { Results } from './results'; +import { Article } from './article'; +import { ArticleService } from './article.service'; + +describe('ArticleService', () => { + let service: ArticleService; + const entries: EntryCollection = { + total: 1, + skip: 0, + limit: 10, + items: [ + { + metadata: { + tags: [ + { + sys: { + type: 'Link', + linkType: 'Tag', + id: 'django', + }, + }, + { + sys: { + type: 'Link', + linkType: 'Tag', + id: 'python', + }, + }, + ], + }, + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: 'space', + }, + }, + id: '5q42lmaU5qOIb9Uv6UO1LU', + type: 'Entry', + createdAt: '2023-07-16T12:43:45.844Z', + updatedAt: '2023-07-16T18:54:23.261Z', + environment: { + sys: { + id: 'staging', + type: 'Link', + linkType: 'Environment', + }, + }, + revision: 4, + contentType: { + sys: { + type: 'Link', + linkType: 'ContentType', + id: 'article', + }, + }, + locale: 'en-US', + }, + fields: { + title: + 'The Django administration site: One of the reasons why I love Django', + slug: 'the-django-administration-site-one-of-the-reasons-why-i-love-django', + cover: { + metadata: { + tags: [], + }, + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: 'space', + }, + }, + id: '1CbZoQHRwcvjlPwOukHm5G', + type: 'Asset', + createdAt: '2023-07-16T12:42:00.324Z', + updatedAt: '2023-07-16T12:42:00.324Z', + environment: { + sys: { + id: 'staging', + type: 'Link', + linkType: 'Environment', + }, + }, + revision: 1, + locale: 'en-US', + }, + fields: { + title: 'django-admin', + description: '', + file: { + url: '//images.ctfassets.net/gq9ultyr0moo/1CbZoQHRwcvjlPwOukHm5G/92cdc9bd7b088aeab143d470cd045ec9/django-admin.png', + details: { + size: 17512, + image: { + width: 1200, + height: 630, + }, + }, + fileName: 'django-admin.png', + contentType: 'image/png', + }, + }, + }, + abstract: + 'My developer friends call me “Django boy”. This is because, whenever we have to start working on a new project, I consider Django first.', + content: { + data: {}, + content: [ + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: 'The Django administration', + nodeType: 'text', + }, + ], + nodeType: 'heading-2', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: + 'Before trying Django back in 2015, I had used only PHP frameworks like Symfony, Laravel and Codeigniter. One difficulty I had with those was to populate my database so that I could test the interfaces in the early stages of the project. They didn’t provide a way to do that in a graphical user interface. So one had to insert and update data in the database using raw SQL queries or fixtures. Later on, they had some third party projects like Sonata Admin for Symfony that allowed you to add an administration site to your project. But yes, you had to install additional packages in your project and spend a lot of time configuring them.', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: + 'With Django, it wasn’t the case and it is still not the case. The project that is generated by the command ', + nodeType: 'text', + }, + { + data: {}, + marks: [ + { + type: 'code', + }, + ], + value: 'django-admin startproject', + nodeType: 'text', + }, + { + data: {}, + marks: [], + value: + ' will create for you a project with the Django administration site already configured and ready to use. All you have to do is create your model just as usual, and then register them in the admin.', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: + 'Let’s take an example. To follow along, you will need to have Python and Django installed on your computer. You also need to have some basic understanding of programming as this is not a tutorial on Django nor the Django administration site. We are going to create a project, add a model and register that model with Django admin so that we can populate the table represented by the model from the admin interface.', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: { + target: { + metadata: { + tags: [], + }, + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: 'space', + }, + }, + id: '3EObbivEB3UYiKJ4u8a158', + type: 'Entry', + createdAt: '2023-07-16T12:44:52.271Z', + updatedAt: '2023-07-16T12:44:52.271Z', + environment: { + sys: { + id: 'staging', + type: 'Link', + linkType: 'Environment', + }, + }, + revision: 1, + contentType: { + sys: { + type: 'Link', + linkType: 'ContentType', + id: 'githubGist', + }, + }, + locale: 'en-US', + }, + fields: { + name: 'Rest parameter', + url: 'https://gist.github.com/valerymelou/1829e21bc03e7a6809b3530c7a9562dc', + }, + }, + }, + content: [], + nodeType: 'embedded-entry-block', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: 'Creating a model', + nodeType: 'text', + }, + ], + nodeType: 'heading-3', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: + "A Django project is made of applications which are just ways to isolate functionalities in modules. Every model needs to be part of an application. So let's create the application that will contain our model. Change your directory to the one containing your project files (", + nodeType: 'text', + }, + { + data: {}, + marks: [ + { + type: 'code', + }, + ], + value: 'website', + nodeType: 'text', + }, + { + data: {}, + marks: [], + value: ' in my case) and run the command bellow:', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [ + { + type: 'code', + }, + ], + value: '> python manage.py startapp blog', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: '', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + ], + nodeType: 'document', + }, + }, + }, + ], + includes: { + Asset: [ + { + metadata: { + tags: [], + }, + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: 'space', + }, + }, + id: '1CbZoQHRwcvjlPwOukHm5G', + type: 'Asset', + createdAt: '2023-07-16T12:42:00.324Z', + updatedAt: '2023-07-16T12:42:00.324Z', + environment: { + sys: { + id: 'staging', + type: 'Link', + linkType: 'Environment', + }, + }, + revision: 1, + locale: 'en-US', + }, + fields: { + title: 'django-admin', + description: '', + file: { + url: '//images.ctfassets.net/gq9ultyr0moo/1CbZoQHRwcvjlPwOukHm5G/92cdc9bd7b088aeab143d470cd045ec9/django-admin.png', + details: { + size: 17512, + image: { + width: 1200, + height: 630, + }, + }, + fileName: 'django-admin.png', + contentType: 'image/png', + }, + }, + }, + ], + }, + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: ContentfulService, + useValue: { getEntries: () => of(entries) }, + }, + ], + }); + service = TestBed.inject(ArticleService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should get article entries', () => { + service.get({}).subscribe((articles: Results
) => { + expect(articles.items.length === 1); + }); + }); +}); diff --git a/libs/blog/data-access/src/lib/article.service.ts b/libs/blog/data-access/src/lib/article.service.ts new file mode 100644 index 0000000..f5d169b --- /dev/null +++ b/libs/blog/data-access/src/lib/article.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; + +import { Observable, map } from 'rxjs'; +import { Entry, EntryCollection, EntrySkeletonType } from 'contentful'; + +import { ContentfulService } from '@valerymelou/cms/contentful'; + +import { Article } from './article'; +import { Results } from './results'; + +@Injectable({ + providedIn: 'root', +}) +export class ArticleService { + private contentType = 'article'; + + constructor(private contentfulService: ContentfulService) {} + + get(query: { [key: string]: string }): Observable> { + return this.contentfulService.getEntries(this.contentType, query).pipe( + map((entries: EntryCollection) => { + const results: Results
= { + items: [], + skip: entries.skip, + total: entries.total, + limit: entries.limit, + }; + + entries.items.forEach( + (entry: Entry) => { + results.items.push( + Article.fromEntry(entry, entries.includes?.Asset), + ); + }, + ); + + return results; + }), + ); + } +} diff --git a/libs/blog/data-access/src/lib/article.ts b/libs/blog/data-access/src/lib/article.ts new file mode 100644 index 0000000..31071da --- /dev/null +++ b/libs/blog/data-access/src/lib/article.ts @@ -0,0 +1,50 @@ +import { + Asset as ContentfulAsset, + Entry, + EntrySkeletonType, + TagLink, +} from 'contentful'; +import { Asset } from './asset'; +import { Tag } from './tag'; + +export class Article { + title = ''; + slug = ''; + abstract = ''; + cover?: Asset; + createdAt = ''; + updatedAt = ''; + tags: Tag[] = []; + + static fromEntry( + entry: Entry, + assets?: ContentfulAsset[] + ): Article { + const article = new Article(); + article.title = entry.fields['title'] as string; + article.slug = entry.fields['slug'] as string; + article.abstract = entry.fields['abstract'] as string; + article.createdAt = entry.sys.createdAt; + article.updatedAt = entry.sys.updatedAt; + if (entry.fields['cover'] && assets) { + assets.forEach((asset: ContentfulAsset) => { + if ( + asset.sys.id === (entry.fields['cover'] as ContentfulAsset).sys.id + ) { + article.cover = Asset.fromAsset(asset); + } + }); + } + + entry.metadata.tags.forEach((tag: { sys: TagLink }) => { + article.tags.push({ + id: tag.sys.id, + label: tag.sys.id + .split(' ') + .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(), + }); + }); + return article; + } +} diff --git a/libs/blog/data-access/src/lib/asset.ts b/libs/blog/data-access/src/lib/asset.ts new file mode 100644 index 0000000..9c1ae52 --- /dev/null +++ b/libs/blog/data-access/src/lib/asset.ts @@ -0,0 +1,20 @@ +import { Asset as ContentfulAsset, AssetFile } from 'contentful'; + +export class Asset { + title = ''; + description = ''; + contentType = ''; + filename = ''; + url = ''; + + static fromAsset(entry: ContentfulAsset): Asset { + const asset = new Asset(); + asset.title = entry.fields['title'] as string; + asset.description = entry.fields['description'] as string; + asset.contentType = (entry.fields['file'] as AssetFile).contentType; + asset.filename = (entry.fields['file'] as AssetFile).fileName; + asset.url = (entry.fields['file'] as AssetFile).url; + + return asset; + } +} diff --git a/libs/blog/data-access/src/lib/results.ts b/libs/blog/data-access/src/lib/results.ts new file mode 100644 index 0000000..8d556f3 --- /dev/null +++ b/libs/blog/data-access/src/lib/results.ts @@ -0,0 +1,6 @@ +export interface Results { + items: T[]; + skip: number; + limit: number; + total: number; +} diff --git a/libs/blog/data-access/src/lib/tag.service.spec.ts b/libs/blog/data-access/src/lib/tag.service.spec.ts new file mode 100644 index 0000000..dd37b72 --- /dev/null +++ b/libs/blog/data-access/src/lib/tag.service.spec.ts @@ -0,0 +1,77 @@ +import { TestBed } from '@angular/core/testing'; + +import { TagService } from './tag.service'; +import { TagCollection } from 'contentful'; +import { of } from 'rxjs'; +import { ContentfulService } from '@valerymelou/cms/contentful'; +import { Results } from './results'; +import { Tag } from './tag'; + +describe('TagService', () => { + let service: TagService; + const tags: TagCollection = { + total: 1, + skip: 0, + limit: 100, + items: [ + { + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: 'space', + }, + }, + id: 'django', + type: 'Tag', + createdAt: '2022-02-21T16:31:29.765Z', + updatedAt: '2022-02-21T16:31:29.765Z', + environment: { + sys: { + id: 'staging', + type: 'Link', + linkType: 'Environment', + }, + }, + createdBy: { + sys: { + type: 'Link', + linkType: 'User', + id: '4up3TfP0Xz6oJ44I32FFk8', + }, + }, + updatedBy: { + sys: { + type: 'Link', + linkType: 'User', + id: '4up3TfP0Xz6oJ44I32FFk8', + }, + }, + version: 1, + visibility: 'public', + }, + name: 'Django', + }, + ], + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: ContentfulService, useValue: { getTags: () => of(tags) } }, + ], + }); + service = TestBed.inject(TagService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should get all tags', () => { + service.getAll().subscribe((tags: Results) => { + expect(tags.items.length).toEqual(1); + }); + }); +}); diff --git a/libs/blog/data-access/src/lib/tag.service.ts b/libs/blog/data-access/src/lib/tag.service.ts new file mode 100644 index 0000000..44ab121 --- /dev/null +++ b/libs/blog/data-access/src/lib/tag.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { ContentfulService } from '@valerymelou/cms/contentful'; +import { Observable, map } from 'rxjs'; +import { Tag } from './tag'; +import { Tag as ContentfulTag, TagCollection } from 'contentful'; +import { Results } from './results'; + +@Injectable({ + providedIn: 'root', +}) +export class TagService { + constructor(private contentfulService: ContentfulService) {} + + getAll(): Observable> { + return this.contentfulService.getTags().pipe( + map((collection: TagCollection) => { + const tags: Results = { + items: [], + limit: 0, + skip: 0, + total: 0, + }; + collection.items.forEach((tag: ContentfulTag) => { + tags.items.push({ + id: tag.sys.id, + label: tag.name, + }); + }); + return tags; + }), + ); + } +} diff --git a/libs/blog/data-access/src/lib/tag.ts b/libs/blog/data-access/src/lib/tag.ts new file mode 100644 index 0000000..5415d80 --- /dev/null +++ b/libs/blog/data-access/src/lib/tag.ts @@ -0,0 +1,4 @@ +export interface Tag { + id: string; + label: string; +} diff --git a/libs/blog/data-access/src/test-setup.ts b/libs/blog/data-access/src/test-setup.ts new file mode 100644 index 0000000..ab1eeeb --- /dev/null +++ b/libs/blog/data-access/src/test-setup.ts @@ -0,0 +1,8 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; +import 'jest-preset-angular/setup-jest'; diff --git a/libs/blog/data-access/tsconfig.json b/libs/blog/data-access/tsconfig.json new file mode 100644 index 0000000..5cf0a16 --- /dev/null +++ b/libs/blog/data-access/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/blog/data-access/tsconfig.lib.json b/libs/blog/data-access/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/libs/blog/data-access/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/blog/data-access/tsconfig.spec.json b/libs/blog/data-access/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/libs/blog/data-access/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/cms/contentful/.eslintrc.json b/libs/cms/contentful/.eslintrc.json new file mode 100644 index 0000000..1f56884 --- /dev/null +++ b/libs/cms/contentful/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "vmelou", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "vmelou", + "style": "kebab-case" + } + ] + }, + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ] + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/cms/contentful/README.md b/libs/cms/contentful/README.md new file mode 100644 index 0000000..74341d5 --- /dev/null +++ b/libs/cms/contentful/README.md @@ -0,0 +1,7 @@ +# cms-contentful + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test cms-contentful` to execute the unit tests. diff --git a/libs/cms/contentful/jest.config.ts b/libs/cms/contentful/jest.config.ts new file mode 100644 index 0000000..6c34e26 --- /dev/null +++ b/libs/cms/contentful/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'cms-contentful', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/cms/contentful', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/cms/contentful/project.json b/libs/cms/contentful/project.json new file mode 100644 index 0000000..f0610ae --- /dev/null +++ b/libs/cms/contentful/project.json @@ -0,0 +1,34 @@ +{ + "name": "cms-contentful", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/cms/contentful/src", + "prefix": "vmelou", + "tags": [], + "projectType": "library", + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/cms/contentful/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "libs/cms/contentful/**/*.ts", + "libs/cms/contentful/**/*.html" + ] + } + } + } +} diff --git a/libs/cms/contentful/src/index.ts b/libs/cms/contentful/src/index.ts new file mode 100644 index 0000000..f08d975 --- /dev/null +++ b/libs/cms/contentful/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/contentful.service'; +export * from './lib/tokens'; diff --git a/libs/cms/contentful/src/lib/contentful.service.spec.ts b/libs/cms/contentful/src/lib/contentful.service.spec.ts new file mode 100644 index 0000000..04a6176 --- /dev/null +++ b/libs/cms/contentful/src/lib/contentful.service.spec.ts @@ -0,0 +1,396 @@ +import { TestBed } from '@angular/core/testing'; +import { EntryCollection, EntrySkeletonType, TagCollection } from 'contentful'; + +import { ContentfulService } from './contentful.service'; +import { + CONTENTFUL_ACCESS_TOKEN, + CONTENTFUL_ENVIRONMENT, + CONTENTFUL_SPACE, +} from './tokens'; + +describe('ContentfulService', () => { + let service: ContentfulService; + const entries: EntryCollection = { + total: 1, + skip: 0, + limit: 10, + items: [ + { + metadata: { + tags: [ + { + sys: { + type: 'Link', + linkType: 'Tag', + id: 'django', + }, + }, + { + sys: { + type: 'Link', + linkType: 'Tag', + id: 'python', + }, + }, + ], + }, + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: 'space', + }, + }, + id: '5q42lmaU5qOIb9Uv6UO1LU', + type: 'Entry', + createdAt: '2023-07-16T12:43:45.844Z', + updatedAt: '2023-07-16T18:54:23.261Z', + environment: { + sys: { + id: 'staging', + type: 'Link', + linkType: 'Environment', + }, + }, + revision: 4, + contentType: { + sys: { + type: 'Link', + linkType: 'ContentType', + id: 'article', + }, + }, + locale: 'en-US', + }, + fields: { + title: + 'The Django administration site: One of the reasons why I love Django', + slug: 'the-django-administration-site-one-of-the-reasons-why-i-love-django', + cover: { + metadata: { + tags: [], + }, + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: 'space', + }, + }, + id: '1CbZoQHRwcvjlPwOukHm5G', + type: 'Asset', + createdAt: '2023-07-16T12:42:00.324Z', + updatedAt: '2023-07-16T12:42:00.324Z', + environment: { + sys: { + id: 'staging', + type: 'Link', + linkType: 'Environment', + }, + }, + revision: 1, + locale: 'en-US', + }, + fields: { + title: 'django-admin', + description: '', + file: { + url: '//images.ctfassets.net/gq9ultyr0moo/1CbZoQHRwcvjlPwOukHm5G/92cdc9bd7b088aeab143d470cd045ec9/django-admin.png', + details: { + size: 17512, + image: { + width: 1200, + height: 630, + }, + }, + fileName: 'django-admin.png', + contentType: 'image/png', + }, + }, + }, + abstract: + 'My developer friends call me “Django boy”. This is because, whenever we have to start working on a new project, I consider Django first.', + content: { + data: {}, + content: [ + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: 'The Django administration', + nodeType: 'text', + }, + ], + nodeType: 'heading-2', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: + 'Before trying Django back in 2015, I had used only PHP frameworks like Symfony, Laravel and Codeigniter. One difficulty I had with those was to populate my database so that I could test the interfaces in the early stages of the project. They didn’t provide a way to do that in a graphical user interface. So one had to insert and update data in the database using raw SQL queries or fixtures. Later on, they had some third party projects like Sonata Admin for Symfony that allowed you to add an administration site to your project. But yes, you had to install additional packages in your project and spend a lot of time configuring them.', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: + 'With Django, it wasn’t the case and it is still not the case. The project that is generated by the command ', + nodeType: 'text', + }, + { + data: {}, + marks: [ + { + type: 'code', + }, + ], + value: 'django-admin startproject', + nodeType: 'text', + }, + { + data: {}, + marks: [], + value: + ' will create for you a project with the Django administration site already configured and ready to use. All you have to do is create your model just as usual, and then register them in the admin.', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: + 'Let’s take an example. To follow along, you will need to have Python and Django installed on your computer. You also need to have some basic understanding of programming as this is not a tutorial on Django nor the Django administration site. We are going to create a project, add a model and register that model with Django admin so that we can populate the table represented by the model from the admin interface.', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: { + target: { + metadata: { + tags: [], + }, + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: 'space', + }, + }, + id: '3EObbivEB3UYiKJ4u8a158', + type: 'Entry', + createdAt: '2023-07-16T12:44:52.271Z', + updatedAt: '2023-07-16T12:44:52.271Z', + environment: { + sys: { + id: 'staging', + type: 'Link', + linkType: 'Environment', + }, + }, + revision: 1, + contentType: { + sys: { + type: 'Link', + linkType: 'ContentType', + id: 'githubGist', + }, + }, + locale: 'en-US', + }, + fields: { + name: 'Rest parameter', + url: 'https://gist.github.com/valerymelou/1829e21bc03e7a6809b3530c7a9562dc', + }, + }, + }, + content: [], + nodeType: 'embedded-entry-block', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: 'Creating a model', + nodeType: 'text', + }, + ], + nodeType: 'heading-3', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: + "A Django project is made of applications which are just ways to isolate functionalities in modules. Every model needs to be part of an application. So let's create the application that will contain our model. Change your directory to the one containing your project files (", + nodeType: 'text', + }, + { + data: {}, + marks: [ + { + type: 'code', + }, + ], + value: 'website', + nodeType: 'text', + }, + { + data: {}, + marks: [], + value: ' in my case) and run the command bellow:', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [ + { + type: 'code', + }, + ], + value: '> python manage.py startapp blog', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + { + data: {}, + content: [ + { + data: {}, + marks: [], + value: '', + nodeType: 'text', + }, + ], + nodeType: 'paragraph', + }, + ], + nodeType: 'document', + }, + }, + }, + ], + includes: {}, + }; + const tags: TagCollection = { + total: 1, + skip: 0, + limit: 100, + items: [ + { + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: 'space', + }, + }, + id: 'django', + type: 'Tag', + createdAt: '2022-02-21T16:31:29.765Z', + updatedAt: '2022-02-21T16:31:29.765Z', + environment: { + sys: { + id: 'staging', + type: 'Link', + linkType: 'Environment', + }, + }, + createdBy: { + sys: { + type: 'Link', + linkType: 'User', + id: '4up3TfP0Xz6oJ44I32FFk8', + }, + }, + updatedBy: { + sys: { + type: 'Link', + linkType: 'User', + id: '4up3TfP0Xz6oJ44I32FFk8', + }, + }, + version: 1, + visibility: 'public', + }, + name: 'Django', + }, + ], + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: CONTENTFUL_SPACE, useValue: 'space' }, + { provide: CONTENTFUL_ACCESS_TOKEN, useValue: 'access token' }, + { provide: CONTENTFUL_ENVIRONMENT, useValue: 'test' }, + ], + }); + service = TestBed.inject(ContentfulService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should transform the promise of entries into an observable', () => { + const getEntriesSpy = jest + .spyOn(service['cdaClient'], 'getEntries') + .mockImplementation(() => Promise.resolve(entries)); + + service + .getEntries('article', {}) + .subscribe( + (collection: EntryCollection) => { + expect(collection.items === entries.items); + } + ); + + expect(getEntriesSpy).toHaveBeenCalled(); + }); + + it('should get the list of tags', () => { + const getTagsSpy = jest + .spyOn(service['cdaClient'], 'getTags') + .mockImplementation(() => Promise.resolve(tags)); + + service.getTags().subscribe((collection: TagCollection) => { + expect(collection.items === tags.items); + }); + + expect(getTagsSpy).toHaveBeenCalled(); + }); +}); diff --git a/libs/cms/contentful/src/lib/contentful.service.ts b/libs/cms/contentful/src/lib/contentful.service.ts new file mode 100644 index 0000000..fed260e --- /dev/null +++ b/libs/cms/contentful/src/lib/contentful.service.ts @@ -0,0 +1,71 @@ +import { inject, Injectable } from '@angular/core'; +import { from, Observable } from 'rxjs'; +import { + ContentfulClientApi, + EntryCollection, + EntrySkeletonType, + TagCollection, + createClient, +} from 'contentful'; +import { + CONTENTFUL_ACCESS_TOKEN, + CONTENTFUL_ENVIRONMENT, + CONTENTFUL_SPACE, +} from './tokens'; + +@Injectable({ + providedIn: 'root', +}) +export class ContentfulService { + private cdaClient: ContentfulClientApi; + + constructor() { + const space = inject(CONTENTFUL_SPACE); + const accessToken = inject(CONTENTFUL_ACCESS_TOKEN); + const environment = inject(CONTENTFUL_ENVIRONMENT); + this.cdaClient = createClient({ space, accessToken, environment }); + } + + /** + * Query the contentful content type and return an observable of corresponding entries. + * + * @param contentType The Contentful content type to get the entries of + * @param query Query to filter the entries + * @returns Observable of EntryCollection + */ + getEntries( + contentType: string, + query?: { [key: string]: string }, + ): Observable> { + let defaultQuery: { [key: string]: string } = { + content_type: contentType, + limit: '10', + skip: '0', + order: '-sys.createdAt', + }; + defaultQuery = Object.assign(defaultQuery, query); + + return from( + this.cdaClient + .getEntries(defaultQuery) + .then( + (entries: EntryCollection) => { + return entries; + }, + ), + ); + } + + /** + * Returns an observable of Contentful tags. + * + * @returns Observable of TagCollection + */ + getTags(): Observable { + return from( + this.cdaClient.getTags().then((tags: TagCollection) => { + return tags; + }), + ); + } +} diff --git a/libs/cms/contentful/src/lib/tokens.ts b/libs/cms/contentful/src/lib/tokens.ts new file mode 100644 index 0000000..42dc048 --- /dev/null +++ b/libs/cms/contentful/src/lib/tokens.ts @@ -0,0 +1,9 @@ +import { InjectionToken } from '@angular/core'; + +export const CONTENTFUL_SPACE = new InjectionToken('Contentful space'); +export const CONTENTFUL_ACCESS_TOKEN = new InjectionToken( + 'Contentful access token' +); +export const CONTENTFUL_ENVIRONMENT = new InjectionToken( + 'Contentful environment' +); diff --git a/libs/cms/contentful/src/test-setup.ts b/libs/cms/contentful/src/test-setup.ts new file mode 100644 index 0000000..ab1eeeb --- /dev/null +++ b/libs/cms/contentful/src/test-setup.ts @@ -0,0 +1,8 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; +import 'jest-preset-angular/setup-jest'; diff --git a/libs/cms/contentful/tsconfig.json b/libs/cms/contentful/tsconfig.json new file mode 100644 index 0000000..5cf0a16 --- /dev/null +++ b/libs/cms/contentful/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/cms/contentful/tsconfig.lib.json b/libs/cms/contentful/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/libs/cms/contentful/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/cms/contentful/tsconfig.spec.json b/libs/cms/contentful/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/libs/cms/contentful/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/package.json b/package.json index 3d0611f..8275257 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@ng-icons/bootstrap-icons": "^27.3.1", "@ng-icons/core": "^27.3.1", "@ng-icons/material-icons": "^27.3.1", + "contentful": "^10.11.3", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.3" diff --git a/tsconfig.base.json b/tsconfig.base.json index 3c89815..834586a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,7 +20,9 @@ "@valerymelou/pages/home": ["libs/pages/home/src/index.ts"], "@valerymelou/pages/projects": ["libs/pages/projects/src/index.ts"], "@valerymelou/shared/layout": ["libs/shared/layout/src/index.ts"], - "@valerymelou/shared/ui": ["libs/shared/ui/src/index.ts"] + "@valerymelou/shared/ui": ["libs/shared/ui/src/index.ts"], + "@valerymelou/blob/data-access": ["libs/blog/data-access/src/index.ts"], + "@valerymelou/cms/contentful": ["libs/cms/contentful/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/yarn.lock b/yarn.lock index 925da94..de23558 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1487,6 +1487,19 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@contentful/content-source-maps@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@contentful/content-source-maps/-/content-source-maps-0.5.0.tgz#3561c705f49ec3411aa90ff8a3606ceb5da1fc5b" + integrity sha512-Ui14m5QvgFWteAXsv5pmFtbm2deGUJrn1WPHoBa79SK3Z3nV47wQb3/aDZhnXushgpJlk2HmH32Fda/0XZqq1w== + dependencies: + "@vercel/stega" "^0.1.2" + json-pointer "^0.6.2" + +"@contentful/rich-text-types@^16.0.2": + version "16.5.0" + resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-16.5.0.tgz#f82f38d2131ebae10458990cbb7574d9b77b77e7" + integrity sha512-Z5fls/6Hs+ETq6vx+sfiq+n+z3dwurPVJukgLvThdvzs4VlChoPkRIKr4WrBdjzLYqUspawlf7XvtuUpp32glw== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -3355,6 +3368,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@vercel/stega@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@vercel/stega/-/stega-0.1.2.tgz#0c20c5c9419c4288b1de58a64b5f9f26c763b25f" + integrity sha512-P7mafQXjkrsoyTRppnt0N21udKS9wUmLXHRyP9saLXLHw32j/FgUJ3FscSWgvSqRs4cj7wKZtwqJEvWJ2jbGmA== + "@vitejs/plugin-basic-ssl@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz#8b840305a6b48e8764803435ec0c716fa27d3802" @@ -3789,6 +3807,15 @@ axios@^1.6.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.6.7: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.0.0.tgz#04a4c90dce33cc5d606c76d6216e3b250ff70dab" @@ -4388,6 +4415,37 @@ content-type@~1.0.4, content-type@~1.0.5: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== +contentful-resolve-response@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/contentful-resolve-response/-/contentful-resolve-response-1.8.1.tgz#b44ff13e12fab7deb00ef6216d8a7171bdda0395" + integrity sha512-VXGK2c8dBIGcRCknqudKmkDr2PzsUYfjLN6hhx71T09UzoXOdA/c0kfDhsf/BBCBWPWcLaUgaJEFU0lCo45TSg== + dependencies: + fast-copy "^2.1.7" + +contentful-sdk-core@^8.1.0: + version "8.1.3" + resolved "https://registry.yarnpkg.com/contentful-sdk-core/-/contentful-sdk-core-8.1.3.tgz#c6c74cea732cd0bf395cc39aac52aa11bb388382" + integrity sha512-jAek5yGhpk3OJC+lWeq4AZIHO3lxELNMyETDU0fYkPnRS4wHCjbNv5cSk84Rbo7NyH5CkfgzGszvc0GDSXtg0w== + dependencies: + fast-copy "^2.1.7" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + p-throttle "^4.1.1" + qs "^6.11.2" + +contentful@^10.11.3: + version "10.11.3" + resolved "https://registry.yarnpkg.com/contentful/-/contentful-10.11.3.tgz#9ca638746b058dfcd40ed894af305c5e85e33d4a" + integrity sha512-sBI2fCfw101JOSJvjVS2rBpP3HwGl3S2lQPhYwdKjrLepktdJs+gxEyGnMWhOJ36Di5aObIrc0PSA4jAftDB/Q== + dependencies: + "@contentful/content-source-maps" "^0.5.0" + "@contentful/rich-text-types" "^16.0.2" + axios "^1.6.7" + contentful-resolve-response "^1.8.1" + contentful-sdk-core "^8.1.0" + json-stringify-safe "^5.0.1" + type-fest "^4.0.0" + convert-source-map@^1.5.1, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -5370,6 +5428,11 @@ external-editor@^3.1.0: iconv-lite "^0.4.24" tmp "^0.0.33" +fast-copy@^2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-2.1.7.tgz#affc9475cb4b555fb488572b2a44231d0c9fa39e" + integrity sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -5534,6 +5597,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.15.6: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== +foreach@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e" + integrity sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg== + foreground-child@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" @@ -6804,6 +6872,13 @@ json-parse-even-better-errors@^3.0.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz#02bb29fb5da90b5444581749c22cedd3597c6cb0" integrity sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg== +json-pointer@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/json-pointer/-/json-pointer-0.6.2.tgz#f97bd7550be5e9ea901f8c9264c9d436a22a93cd" + integrity sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw== + dependencies: + foreach "^2.0.4" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -6819,6 +6894,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" @@ -7021,6 +7101,16 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -7791,6 +7881,11 @@ p-retry@^4.5.0: "@types/retry" "0.12.0" retry "^0.13.1" +p-throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/p-throttle/-/p-throttle-4.1.1.tgz#80b1fbd358af40a8bfa1667f9dc8b72b714ad692" + integrity sha512-TuU8Ato+pRTPJoDzYD4s7ocJYcNSEZRvlxoq3hcPI2kZDZ49IQ1Wkj7/gDJc3X7XiEAAvRGtDzdXJI0tC3IL1g== + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -8391,6 +8486,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.11.2: + version "6.12.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" + integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== + dependencies: + side-channel "^1.0.6" + qs@^6.4.0: version "6.12.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.0.tgz#edd40c3b823995946a8a0b1f208669c7a200db77" @@ -9574,6 +9676,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^4.0.0: + version "4.18.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.18.2.tgz#8d765c42e7280a11f4d04fb77a00dacc417c8b05" + integrity sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"