diff --git a/app/microservice/register.json b/app/microservice/register.json index e41a1b2..84e4c68 100644 --- a/app/microservice/register.json +++ b/app/microservice/register.json @@ -160,6 +160,14 @@ "method": "GET", "path": "/api/v1/area/:id/alerts" } + },{ + "path": "/v1/area/find/:id", + "method": "GET", + "authenticated": true, + "redirect": { + "method": "GET", + "path": "/api/v1/area/find/:id" + } },{ "path": "/v1/download-tiles/:geostoreId/:minZoom/:maxZoom", "method": "GET", diff --git a/app/src/models/area.model.js b/app/src/models/area.model.js index 4849f52..447eb4c 100644 --- a/app/src/models/area.model.js +++ b/app/src/models/area.model.js @@ -33,7 +33,8 @@ const Area = new Schema({ datasets: [Dataset], createdAt: { type: Date, required: true, default: Date.now }, image: { type: String, required: false, trim: true }, - templateId: { type: String, trim: true, required: false } + templateId: { type: String, trim: true, required: false }, + templateIds: { type: Array, required: false, default: [] } }); diff --git a/app/src/routes/api/v1/area.router.js b/app/src/routes/api/v1/area.router.js index 049a990..e00ea96 100644 --- a/app/src/routes/api/v1/area.router.js +++ b/app/src/routes/api/v1/area.router.js @@ -6,6 +6,7 @@ const AreaValidator = require('validators/area.validator'); const AlertsService = require('services/alerts.service'); const TeamService = require('services/team.service'); const s3Service = require('services/s3.service'); +const config = require('config'); const router = new Router({ prefix: '/area', @@ -83,11 +84,22 @@ class AreaRouter { await AreaRouter.saveArea(ctx, ctx.params.userId); } + // returns all area ID's which include the templateID given + static async getAOIs(ctx) { + const area = await AreaModel.find({ $or: [{ templateIds: ctx.params.id }, { templateId: ctx.params.id }] }, { + _id: 1 + }); + ctx.body = AreaSerializer.serialize(area); + } + + static async saveArea(ctx, userId) { logger.info('Saving area'); let image = ''; if (ctx.request.files && ctx.request.files.image) { image = await s3Service.uploadFile(ctx.request.files.image.path, ctx.request.files.image.name); + } else { + image = config.get('image.PLACEHOLDER'); } let datasets = []; if (ctx.request.body.datasets) { @@ -152,11 +164,34 @@ class AreaRouter { if (ctx.request.body.datasets) { area.datasets = JSON.parse(ctx.request.body.datasets); } + // image fall back if (files && files.image) { area.image = await s3Service.uploadFile(files.image.path, files.image.name); + } else { + area.image = config.get('image.PLACEHOLDER'); } if (typeof ctx.request.body.templateId !== 'undefined') { - area.templateId = ctx.request.body.templateId; + + if (ctx.request.body.override === true) { + logger.debug('debug', ctx.request.body.templateId); + + const { templateId } = ctx.request.body; + await AreaModel.findOneAndUpdate( + { _id: ctx.params.id }, + { $pull: { templateIds: templateId.toString() } } + ); + } else { + // backward compatibility - templateIds is the new version + // templateId is for backward compatibility + const templateIds = ctx.request.body.templateId; + if (templateIds.constructor === Array) { + // eslint-disable-next-line prefer-destructuring + area.templateId = ctx.request.body.templateId[0]; + } else { + area.templateId = ctx.request.body.templateId; + } + area.templateIds.addToSet(ctx.request.body.templateId); + } } area.updatedDate = Date.now; @@ -276,6 +311,7 @@ async function unwrapJSONStrings(ctx, next) { router.post('/', loggedUserToState, unwrapJSONStrings, AreaValidator.create, AreaRouter.save); router.patch('/:id', loggedUserToState, checkPermission, unwrapJSONStrings, AreaValidator.update, AreaRouter.update); +router.get('/find/:id', loggedUserToState, AreaRouter.getAOIs); router.get('/', loggedUserToState, AreaRouter.getAll); router.get('/fw', loggedUserToState, AreaRouter.getFWAreas); router.post('/fw/:userId', loggedUserToState, AreaValidator.create, AreaRouter.saveByUserId); diff --git a/app/src/serializers/area.serializer.js b/app/src/serializers/area.serializer.js index 76a60c7..36966dd 100644 --- a/app/src/serializers/area.serializer.js +++ b/app/src/serializers/area.serializer.js @@ -12,7 +12,8 @@ const areaSerializer = new JSONAPISerializer('area', { 'datasets', 'use', 'iso', - 'templateId' + 'templateId', + 'templateIds' ], resource: { attributes: ['type', 'content'] diff --git a/app/src/services/download.service.js b/app/src/services/download.service.js index dbcca63..b460797 100644 --- a/app/src/services/download.service.js +++ b/app/src/services/download.service.js @@ -127,11 +127,21 @@ class DownloadService { } } if (promises.length > 0) { - await Promise.all(promises); + // map the promises which error to the error promise array, retry those requests. + // If they fail then catch them as null + // This will result in missing tiles however will not fall over + const errors = []; + await Promise.all(promises.map((p) => p.catch(() => errors.push(p)))); + if (errors.length > 0) { + await Promise.all(errors.map((p) => p.catch(() => null))); + logger.debug(errors); + } promises = null; } // eslint-disable-next-line no-empty - } catch (err) {} + } catch (err) { + logger.debug(err); + } await DownloadService.zipFolder(tmpobj.name, `${tmpDownload.name}/download.zip`); logger.info('Removing file ', tmpobj.name); await DownloadService.removeFolder(tmpobj.name); diff --git a/app/test/e2e/utils/helpers.js b/app/test/e2e/utils/helpers.js index 35eb140..b4af121 100644 --- a/app/test/e2e/utils/helpers.js +++ b/app/test/e2e/utils/helpers.js @@ -9,6 +9,7 @@ const createArea = (anotherData = {}) => ({ image: '', createdAt: new Date(), wdpaid: 1, + templateId: 'updatedTemplateId', ...anotherData }); diff --git a/app/test/e2e/v1/update-area.spec.js b/app/test/e2e/v1/update-area.spec.js index f46e257..bed8f4a 100644 --- a/app/test/e2e/v1/update-area.spec.js +++ b/app/test/e2e/v1/update-area.spec.js @@ -161,6 +161,64 @@ describe('Update area - V1', () => { }); }); + it('Update area with multiple template ids who is logged in', async () => { + const testArea = await new Area(createArea({ userId: USERS.USER.id })).save(); + + const response = await requester + .patch(`/api/v1/area/${testArea.id}`) + .send({ + loggedUser: USERS.USER, + name: 'Portugal area', + application: 'rw', + geostore: '713899292fc118a915741728ef84a2a7', + wdpaid: 3, + use: { + id: 'bbb', + name: 'updated name' + }, + iso: { + country: 'updatedCountryIso', + region: 'updatedRegionIso' + }, + datasets: '[{"slug":"viirs","name":"VIIRS","startDate":"7","endDate":"1","lastUpdate":1513793462776.0,"_id":"5a3aa9eb98b5910011731f66","active":true,"cache":true}]', + templateId: 'firstUpdatedID' + }); + + response.status.should.equal(200); + + response.body.should.have.property('data').and.be.an('object'); + response.body.data.should.have.property('type').and.equal('area'); + response.body.data.should.have.property('id').and.equal(testArea.id); + response.body.data.attributes.should.have.property('name').and.equal('Portugal area'); + response.body.data.attributes.should.have.property('application').and.equal('rw'); + response.body.data.attributes.should.have.property('geostore').and.equal('713899292fc118a915741728ef84a2a7'); + response.body.data.attributes.should.have.property('userId').and.equal(testArea.userId); + response.body.data.attributes.should.have.property('wdpaid').and.equal(3); + response.body.data.attributes.should.have.property('use').and.deep.equal({ + id: 'bbb', + name: 'updated name' + }); + response.body.data.attributes.should.have.property('iso').and.deep.equal({ + country: 'updatedCountryIso', + region: 'updatedRegionIso' + }); + response.body.data.attributes.should.have.property('createdAt'); + response.body.data.attributes.should.have.property('datasets').and.be.an('array').and.length(1); + response.body.data.attributes.datasets[0].should.deep.equal({ + cache: true, + active: true, + _id: '5a3aa9eb98b5910011731f66', + slug: 'viirs', + name: 'VIIRS', + startDate: '7', + endDate: '1', + lastUpdate: 1513793462776 + }); + response.body.data.attributes.should.have.property('templateId').and.equal('firstUpdatedID'); + response.body.data.attributes.should.have.property('templateIds').and.be.an('array'); + }); + + afterEach(async () => { if (!nock.isDone()) { throw new Error(`Not all nock interceptors were used: ${nock.pendingMocks()}`); diff --git a/config/default.json b/config/default.json index 3c34a7b..1d0a366 100644 --- a/config/default.json +++ b/config/default.json @@ -15,6 +15,9 @@ "host": "127.0.0.1", "port": 27017 }, + "image": { + "PLACEHOLDER": "https://glowvarietyshow.com/wp-content/uploads/2017/03/placeholder-image.jpg" + }, "gladDataset": "f8662607-02cc-4bfb-9bb3-06b25c9ecade", "viirsDataset": "f8662607-02cc-4bfb-9bb3-06b25c9ecade", "viirsDatasetTableName": "vnp14imgtdl_nrt_global_7d", diff --git a/k8s/production/deployment.yaml b/k8s/production/deployment.yaml index 7e2282d..56b36ce 100644 --- a/k8s/production/deployment.yaml +++ b/k8s/production/deployment.yaml @@ -1,7 +1,7 @@ apiVersion: extensions/v1beta1 kind: Deployment metadata: - namespace: gfw + namespace: fw labels: name: {name} app: gfw @@ -10,9 +10,25 @@ spec: revisionHistoryLimit: 2 template: metadata: + annotations: + chaos.alpha.kubernetes.io/enabled: "true" labels: name: {name} spec: + tolerations: + - key: "type" + operator: "Equal" + value: "gfw" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: type + operator: In + values: + - gfw containers: - name: {name} image: vizzuality/{name} @@ -28,24 +44,29 @@ spec: - start env: - name: PORT - value: "4100" + value: "4400" - name: NODE_ENV value: prod - name: NODE_PATH value: app/src - name: LOCAL_URL - value: http://{name}.gfw.svc.cluster.local:4100 - - name: GLAD_DATASET - value: e663eb09-04de-4f39-b871-35c6c2ed10b5 - - name: VIIRS_DATASET - value: 20cc5eca-8c63-4c41-8e8e-134dcf1e6d76 - - name: VIIRS_DATASET_TABLENAME - value: vnp14imgtdl_nrt_global_7d + value: http://{name}.fw.svc.cluster.local:4400 + - name: QUEUE_PROVIDER + value: redis + - name: QUEUE_NAME + value: mail_prod + - name: WRI_MAIL_RECIPIENTS + value: mweisse@wri.org - name: MONGO_URI valueFrom: secretKeyRef: name: dbsecrets - key: AREA_MONGO_URI + key: FORMS_MONGO_URI + - name: QUEUE_URL + valueFrom: + secretKeyRef: + name: dbsecrets + key: REDIS_URI - name: CT_URL valueFrom: secretKeyRef: @@ -70,34 +91,40 @@ spec: valueFrom: secretKeyRef: name: mssecrets - key: AREA_S3_ACCESS_KEY_ID + key: FORMS_S3_ACCESS_KEY_ID - name: S3_SECRET_ACCESS_KEY valueFrom: secretKeyRef: name: mssecrets - key: AREA_S3_SECRET_ACCESS_KEY + key: FORMS_S3_SECRET_ACCESS_KEY - name: S3_BUCKET value: forest-watcher-files - - name: AOI_DATA_S3_ACCESS_KEY_ID + - name: GOOGLE_PRIVATE_KEY valueFrom: secretKeyRef: name: mssecrets - key: AREA_AOI_DATA_S3_ACCESS_KEY_ID - - name: AOI_DATA_S3_SECRET_ACCESS_KEY + key: FORMS_GOOGLE_PRIVATE_KEY + - name: GOOGLE_PROJECT_EMAIL valueFrom: secretKeyRef: name: mssecrets - key: AREA_AOI_DATA_S3_SECRET_ACCESS_KEY - - name: AOI_DATA_S3_BUCKET - value: gfw-pipelines + key: FORMS_GOOGLE_PROJECT_EMAIL + - name: TARGET_SHEET_ID + value: 1oCRTDUlaaadA_xVCWTQ9BaCLxY8do0uSQYGLXu0fQ1k + - name: TARGET_SHEET_INDEX + value: "1" + - name: LEGACY_TEMPLATE_ID + value: "597b0f55856351000b087c9c" + - name: DEFAULT_TEMPLATE_ID + value: "59b6a26b138f260012e9fdeb" ports: - - containerPort: 4100 + - containerPort: 4400 readinessProbe: httpGet: scheme: HTTP path: /healthcheck - port: 4100 + port: 4400 initialDelaySeconds: 30 timeoutSeconds: 5 periodSeconds: 15 @@ -105,7 +132,7 @@ spec: httpGet: scheme: HTTP path: /healthcheck - port: 4100 + port: 4400 failureThreshold: 3 initialDelaySeconds: 30 timeoutSeconds: 5 diff --git a/package.json b/package.json index 84660f1..e2e6551 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,11 @@ "koa-send": "^5.0.0", "koa-simple-healthcheck": "0.0.1", "koa-validate": "^1.0.7", + "lint": "^0.7.0", "moment": "^2.10.6", - "mustache": "^2.3.0", "mongoose": "^5.7.11", "mongoose-history": "^0.8.0", + "mustache": "^2.3.0", "ngeohash": "^0.6.0", "request": "^2.79.0", "request-promise": "^4.1.1",