From 37587eb1e204b416a1c30b434acd3a8c312b0ac4 Mon Sep 17 00:00:00 2001 From: Jaakko Lappalainen Date: Mon, 23 May 2022 18:16:28 +0100 Subject: [PATCH] feat: add posts api --- firebase.json | 13 ++++-- package.json | 3 ++ src/functions/app.controller.spec.ts | 22 ---------- src/functions/app.controller.ts | 12 ------ src/functions/app.module.ts | 23 +++++++--- src/functions/app.service.ts | 8 ---- src/functions/firestore/firestore.module.ts | 43 +++++++++++++++++++ .../firestore/firestore.providers.ts | 7 +++ src/functions/posts/controller.ts | 17 ++++++++ src/functions/posts/document.ts | 9 ++++ src/functions/posts/module.ts | 10 +++++ src/functions/posts/service.ts | 40 +++++++++++++++++ test/app.e2e-spec.ts | 9 ++-- yarn.lock | 37 +++++++++++++++- 14 files changed, 196 insertions(+), 57 deletions(-) delete mode 100644 src/functions/app.controller.spec.ts delete mode 100644 src/functions/app.controller.ts delete mode 100644 src/functions/app.service.ts create mode 100644 src/functions/firestore/firestore.module.ts create mode 100644 src/functions/firestore/firestore.providers.ts create mode 100644 src/functions/posts/controller.ts create mode 100644 src/functions/posts/document.ts create mode 100644 src/functions/posts/module.ts create mode 100644 src/functions/posts/service.ts diff --git a/firebase.json b/firebase.json index 05b1b0d..3de9cbf 100644 --- a/firebase.json +++ b/firebase.json @@ -18,16 +18,23 @@ }, "emulators": { "auth": { - "port": 9099 + "port": 9099, + "host": "127.0.0.1" }, "functions": { - "port": 5001 + "port": 5001, + "host": "127.0.0.1" }, "hosting": { - "port": 5000 + "port": 5000, + "host": "127.0.0.1" }, "ui": { "enabled": true + }, + "firestore": { + "port": 8080, + "host": "127.0.0.1" } } } diff --git a/package.json b/package.json index 01d2b8b..885d5a7 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,12 @@ "semantic-release": "semantic-release" }, "dependencies": { + "@google-cloud/firestore": "^5.0.2", "@nestjs/common": "^8.0.0", + "@nestjs/config": "^2.0.1", "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.4.5", + "dayjs": "^1.11.2", "express": "^4.18.1", "firebase-admin": "^10.2.0", "firebase-functions": "^3.21.2", diff --git a/src/functions/app.controller.spec.ts b/src/functions/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/src/functions/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/functions/app.controller.ts b/src/functions/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/src/functions/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/functions/app.module.ts b/src/functions/app.module.ts index 8662803..98a86ce 100644 --- a/src/functions/app.module.ts +++ b/src/functions/app.module.ts @@ -1,10 +1,23 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { FirestoreModule } from './firestore/firestore.module'; +import { PostsModule } from './posts/module'; @Module({ - imports: [], - controllers: [AppController], - providers: [AppService], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + FirestoreModule.forRoot({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + keyFilename: configService.get('SA_KEY'), + }), + inject: [ConfigService], + }), + PostsModule, + ], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/src/functions/app.service.ts b/src/functions/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/src/functions/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/functions/firestore/firestore.module.ts b/src/functions/firestore/firestore.module.ts new file mode 100644 index 0000000..38c5c3a --- /dev/null +++ b/src/functions/firestore/firestore.module.ts @@ -0,0 +1,43 @@ +import { Module, DynamicModule } from '@nestjs/common'; +import { Firestore, Settings } from '@google-cloud/firestore'; +import { + FirestoreDatabaseProvider, + FirestoreOptionsProvider, + FirestoreCollectionProviders, +} from './firestore.providers'; + +type FirestoreModuleOptions = { + imports: any[]; + useFactory: (...args: any[]) => Settings; + inject: any[]; +}; + +@Module({}) +export class FirestoreModule { + static forRoot(options: FirestoreModuleOptions): DynamicModule { + const optionsProvider = { + provide: FirestoreOptionsProvider, + useFactory: options.useFactory, + inject: options.inject, + }; + const dbProvider = { + provide: FirestoreDatabaseProvider, + useFactory: (config) => new Firestore(config), + inject: [FirestoreOptionsProvider], + }; + const collectionProviders = FirestoreCollectionProviders.map( + (providerName) => ({ + provide: providerName, + useFactory: (db) => db.collection(providerName), + inject: [FirestoreDatabaseProvider], + }), + ); + return { + global: true, + module: FirestoreModule, + imports: options.imports, + providers: [optionsProvider, dbProvider, ...collectionProviders], + exports: [dbProvider, ...collectionProviders], + }; + } +} diff --git a/src/functions/firestore/firestore.providers.ts b/src/functions/firestore/firestore.providers.ts new file mode 100644 index 0000000..867b9d6 --- /dev/null +++ b/src/functions/firestore/firestore.providers.ts @@ -0,0 +1,7 @@ +import { PostDocument } from '../posts/document'; + +export const FirestoreDatabaseProvider = 'firestoredb'; +export const FirestoreOptionsProvider = 'firestoreOptions'; +export const FirestoreCollectionProviders: string[] = [ + PostDocument.collectionName, +]; diff --git a/src/functions/posts/controller.ts b/src/functions/posts/controller.ts new file mode 100644 index 0000000..51109b0 --- /dev/null +++ b/src/functions/posts/controller.ts @@ -0,0 +1,17 @@ +import { Body, Controller as BaseController, Get, Post } from '@nestjs/common'; +import { PostDocument } from './document'; +import { Service } from './service'; + +@BaseController('posts') +export class Controller { + constructor(private readonly service: Service) {} + + @Get() + findAll(): Promise { + return this.service.findAll(); + } + @Post() + public create(@Body() post: PostDocument): Promise { + return this.service.create(post); + } +} diff --git a/src/functions/posts/document.ts b/src/functions/posts/document.ts new file mode 100644 index 0000000..0c632c8 --- /dev/null +++ b/src/functions/posts/document.ts @@ -0,0 +1,9 @@ +import { Timestamp } from '@google-cloud/firestore'; + +export class PostDocument { + static collectionName = 'posts'; + + title: string; + message: string; + date: Timestamp; +} diff --git a/src/functions/posts/module.ts b/src/functions/posts/module.ts new file mode 100644 index 0000000..27c0541 --- /dev/null +++ b/src/functions/posts/module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { Controller } from './controller'; +import { Service } from './service'; + +@Module({ + controllers: [Controller], + providers: [Service], + exports: [Service], +}) +export class PostsModule {} diff --git a/src/functions/posts/service.ts b/src/functions/posts/service.ts new file mode 100644 index 0000000..0108648 --- /dev/null +++ b/src/functions/posts/service.ts @@ -0,0 +1,40 @@ +import { + Injectable, + Inject, + Logger, + InternalServerErrorException, +} from '@nestjs/common'; +import * as dayjs from 'dayjs'; +import { CollectionReference, Timestamp } from '@google-cloud/firestore'; +import { PostDocument } from './document'; + +@Injectable() +export class Service { + private logger: Logger = new Logger(Service.name); + + constructor( + @Inject(PostDocument.collectionName) + private postsCollection: CollectionReference, + ) {} + + async create({ title, message }): Promise { + const t = dayjs(new Date()).valueOf(); + const date = Timestamp.fromMillis(t); + const docRef = this.postsCollection.doc(t.toString()); + await docRef.set({ + title, + message, + date, + }); + const postDoc = await docRef.get(); + const post = postDoc.data(); + return post; + } + + async findAll(): Promise { + const snapshot = await this.postsCollection.get(); + const posts: PostDocument[] = []; + snapshot.forEach((doc) => posts.push(doc.data())); + return posts; + } +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 50cda62..c6ec6d1 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; +import { AppModule } from './../src/functions/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; @@ -15,10 +15,7 @@ describe('AppController (e2e)', () => { await app.init(); }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + it('returns an empty list of posts', () => { + return request(app.getHttpServer()).get('/posts').expect(200).expect([]); }); }); diff --git a/yarn.lock b/yarn.lock index 2cf72af..82be901 100644 --- a/yarn.lock +++ b/yarn.lock @@ -442,6 +442,16 @@ google-gax "^2.24.1" protobufjs "^6.8.6" +"@google-cloud/firestore@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-5.0.2.tgz#36923fde45987f928a220d347f341c5602f9e340" + integrity sha512-xlGcNYaW0nvUMzNn2+pLfbEBVt6oysVqtM89faMgZWkWfEtvIQGS0h5PRdLlcqufNzRCX3yIGv29Pb+03ys+VA== + dependencies: + fast-deep-equal "^3.1.1" + functional-red-black-tree "^1.0.1" + google-gax "^2.24.1" + protobufjs "^6.8.6" + "@google-cloud/paginator@^3.0.7": version "3.0.7" resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" @@ -821,6 +831,16 @@ tslib "2.4.0" uuid "8.3.2" +"@nestjs/config@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-2.0.1.tgz#1b977bb8c1a97637cfc916ab0c0910db047d372e" + integrity sha512-N1sV9YrkLCgqI3plVWt9tccUfMJ3jsjGMl1KN/WDnDkr9jQ4XHt9vnHhdBeclegRyZN2/0TBAqCIEAoD4TaVag== + dependencies: + dotenv "16.0.1" + dotenv-expand "8.0.3" + lodash "4.17.21" + uuid "8.3.2" + "@nestjs/core@^8.0.0": version "8.4.5" resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-8.4.5.tgz#a419259f074a9a1b889540ce2ab46a87b85e9fbf" @@ -2734,6 +2754,11 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5" + integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2914,6 +2939,16 @@ dot-prop@^5.1.0, dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" +dotenv-expand@8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e" + integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg== + +dotenv@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" + integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== + duplexer2@~0.1.0: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -5088,7 +5123,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4: +lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==