diff --git a/.gitignore b/.gitignore index 44a9bf14..a7cda6e4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ /index.d.ts /yarn-error.log package-lock.json + +# IDE artifacts +.vscode diff --git a/package.json b/package.json index d90ef703..ba4226d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unocha/hpc-api-core", - "version": "0.1.0", + "version": "0.2.0", "description": "Core libraries supporting HPC.Tools API Backend", "license": "Apache-2.0", "private": false, @@ -28,7 +28,7 @@ "husky": "^7.0.2", "lint-staged": "^11.2.4", "prettier": "2.4.1", - "typescript": "^4.3.5" + "typescript": "^4.4.4" }, "lint-staged": { "*.{ts,js}": [ diff --git a/src/db/index.ts b/src/db/index.ts index 75b042bb..76df6ae2 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -14,18 +14,22 @@ import expiredData from './models/expiredData'; import form from './models/form'; import governingEntity from './models/governingEntity'; import governingEntityVersion from './models/governingEntityVersion'; +import location from './models/location'; import operation from './models/operation'; import operationCluster from './models/operationCluster'; import participant from './models/participant'; -import project from './models/project'; import plan from './models/plan'; import planEntity from './models/planEntity'; import planEntityVersion from './models/planEntityVersion'; +import planVersion from './models/planVersion'; +import planYear from './models/planYear'; +import project from './models/project'; import projectVersion from './models/projectVersion'; import projectVersionAttachment from './models/projectVersionAttachment'; import projectVersionPlan from './models/projectVersionPlan'; import reportingWindow from './models/reportingWindow'; import reportingWindowAssignment from './models/reportingWindowAssignment'; +import usageYear from './models/usageYear'; import workflowStatusOption from './models/workflowStatusOption'; export default (conn: Knex) => ({ @@ -44,17 +48,21 @@ export default (conn: Knex) => ({ form: form(conn), governingEntity: governingEntity(conn), governingEntityVersion: governingEntityVersion(conn), + location: location(conn), operation: operation(conn), operationCluster: operationCluster(conn), participant: participant(conn), plan: plan(conn), planEntity: planEntity(conn), planEntityVersion: planEntityVersion(conn), + planVersion: planVersion(conn), + planYear: planYear(conn), project: project(conn), projectVersion: projectVersion(conn), projectVersionAttachment: projectVersionAttachment(conn), projectVersionPlan: projectVersionPlan(conn), reportingWindow: reportingWindow(conn), reportingWindowAssignment: reportingWindowAssignment(conn), + usageYear: usageYear(conn), workflowStatusOption: workflowStatusOption(conn), }); diff --git a/src/db/models/attachmentPrototype.ts b/src/db/models/attachmentPrototype.ts index f5b4a697..ef24f049 100644 --- a/src/db/models/attachmentPrototype.ts +++ b/src/db/models/attachmentPrototype.ts @@ -2,6 +2,7 @@ import * as t from 'io-ts'; import { brandedType } from '../../util/io-ts'; import type { Brand } from '../../util/types'; +import { LOCALIZED_STRING } from '../util/datatypes'; import { defineIDModel } from '../util/id-model'; import { PLAN_ID } from './plan'; @@ -26,10 +27,6 @@ export const ATTACHMENT_TYPE = t.keyof({ }); export type AttachmentType = t.TypeOf; -const LOCALIZED_STRING = t.type({ - en: t.string, -}); - const FIELDS = t.array( t.type({ name: LOCALIZED_STRING, @@ -59,10 +56,8 @@ export default defineIDModel({ generated: { id: { kind: 'branded-integer', brand: ATTACHMENT_PROTOTYPE_ID }, }, - optional: { - planId: { kind: 'branded-integer', brand: PLAN_ID }, - }, accidentallyOptional: { + planId: { kind: 'branded-integer', brand: PLAN_ID }, refCode: { kind: 'checked', type: t.string }, type: { kind: 'checked', type: ATTACHMENT_TYPE }, value: { kind: 'checked', type: ATTACHMENT_PROTOTYPE_VALUE }, diff --git a/src/db/models/entityPrototype.ts b/src/db/models/entityPrototype.ts index 2481d34d..5c9665f2 100644 --- a/src/db/models/entityPrototype.ts +++ b/src/db/models/entityPrototype.ts @@ -44,11 +44,11 @@ export default defineIDModel({ generated: { id: { kind: 'branded-integer', brand: ENTITY_PROTOTYPE_ID }, }, + optional: { orderNumber: { kind: 'checked', type: t.number } }, accidentallyOptional: { refCode: { kind: 'checked', type: ENTITY_PROTOTYPE_REF_CODE }, type: { kind: 'checked', type: ENTITY_PROTOTYPE_TYPE }, planId: { kind: 'checked', type: PLAN_ID }, - orderNumber: { kind: 'checked', type: t.number }, value: { kind: 'checked', type: t.unknown }, }, }, diff --git a/src/db/models/governingEntityVersion.ts b/src/db/models/governingEntityVersion.ts index 2dd93aa6..81cf32e0 100644 --- a/src/db/models/governingEntityVersion.ts +++ b/src/db/models/governingEntityVersion.ts @@ -25,6 +25,7 @@ export default defineLegacyVersionedModel({ brand: GOVERNING_ENTITY_VERSION_ID, }, }, + optional: { tags: { kind: 'checked', type: t.array(t.string) } }, accidentallyOptional: { governingEntityId: { kind: 'branded-integer', @@ -33,9 +34,8 @@ export default defineLegacyVersionedModel({ name: { kind: 'checked', type: t.string }, customReference: { kind: 'checked', type: t.string }, value: { kind: 'checked', type: t.unknown }, - tags: { kind: 'checked', type: t.array(t.string) }, }, - required: { + nonNullWithDefault: { overriding: { kind: 'checked', type: t.boolean, diff --git a/src/db/models/location.ts b/src/db/models/location.ts index 2922fb3d..1aeab583 100644 --- a/src/db/models/location.ts +++ b/src/db/models/location.ts @@ -2,6 +2,7 @@ import * as t from 'io-ts'; import { brandedType } from '../../util/io-ts'; import type { Brand } from '../../util/types'; +import { defineIDModel } from '../util/id-model'; export type LocationId = Brand< number, @@ -10,3 +11,39 @@ export type LocationId = Brand< >; export const LOCATION_ID = brandedType(t.number); + +const LOCATION_STATUS = t.keyof({ + active: null, + expired: null, +}); + +export default defineIDModel({ + tableName: 'location', + fields: { + generated: { + id: { kind: 'branded-integer', brand: LOCATION_ID }, + }, + optional: { + externalId: { kind: 'checked', type: t.string }, + name: { kind: 'checked', type: t.string }, + latitude: { kind: 'checked', type: t.number }, + longitude: { kind: 'checked', type: t.number }, + iso3: { kind: 'checked', type: t.string }, + pcode: { kind: 'checked', type: t.string }, + // Even though this column is defined as int8, it is + // fetched as a string by knex, since it is bigint + validOn: { kind: 'checked', type: t.string }, + parentId: { kind: 'branded-integer', brand: LOCATION_ID }, + }, + accidentallyOptional: { + adminLevel: { kind: 'checked', type: t.number }, + status: { + kind: 'checked', + type: LOCATION_STATUS, + }, + itosSync: { kind: 'checked', type: t.boolean }, + }, + }, + idField: 'id', + softDeletionEnabled: false, +}); diff --git a/src/db/models/planEntityVersion.ts b/src/db/models/planEntityVersion.ts index d2b9032b..3e1bb991 100644 --- a/src/db/models/planEntityVersion.ts +++ b/src/db/models/planEntityVersion.ts @@ -2,6 +2,7 @@ import * as t from 'io-ts'; import { brandedType } from '../../util/io-ts'; import type { Brand } from '../../util/types'; +import { LOCALIZED_PLURAL_STRING } from '../util/datatypes'; import { defineLegacyVersionedModel } from '../util/legacy-versioned-model'; import { ENTITY_PROTOTYPE_ID } from './entityPrototype'; import { PLAN_ENTITY_ID } from './planEntity'; @@ -29,12 +30,7 @@ export const PLAN_ENTITY_VERSION_VALUE = t.type({ }), ]) ), - type: t.type({ - en: t.type({ - singular: t.string, - plural: t.string, - }), - }), + type: LOCALIZED_PLURAL_STRING, }); export type PlanEntityVersionValue = t.TypeOf; diff --git a/src/db/models/planVersion.ts b/src/db/models/planVersion.ts index 81fc5579..e09551cc 100644 --- a/src/db/models/planVersion.ts +++ b/src/db/models/planVersion.ts @@ -2,6 +2,10 @@ import * as t from 'io-ts'; import { brandedType } from '../../util/io-ts'; import type { Brand } from '../../util/types'; +import { DATE } from '../util/datatypes'; +import { defineLegacyVersionedModel } from '../util/legacy-versioned-model'; +import { PLAN_ID } from './plan'; +import { PLAN_REPORTING_PERIOD_ID } from './planReportingPeriod'; export type PlanVersionId = Brand< number, @@ -10,3 +14,43 @@ export type PlanVersionId = Brand< >; export const PLAN_VERSION_ID = brandedType(t.number); + +const PLAN_VERSION_CLUSTER_SELECTION_TYPE = t.keyof({ + single: null, + multi: null, +}); + +export default defineLegacyVersionedModel({ + tableName: 'planVersion', + fields: { + generated: { + id: { kind: 'branded-integer', brand: PLAN_VERSION_ID }, + }, + nonNullWithDefault: { + isForHPCProjects: { kind: 'checked', type: t.boolean }, + }, + accidentallyOptional: { + planId: { kind: 'branded-integer', brand: PLAN_ID }, + name: { kind: 'checked', type: t.string }, + startDate: { kind: 'checked', type: DATE }, + endDate: { kind: 'checked', type: DATE }, + }, + optional: { + comments: { kind: 'checked', type: t.string }, + code: { kind: 'checked', type: t.string }, + customLocationCode: { kind: 'checked', type: t.string }, + currentReportingPeriodId: { + kind: 'branded-integer', + brand: PLAN_REPORTING_PERIOD_ID, + }, + lastPublishedReportingPeriodId: { kind: 'checked', type: t.number }, + // Even though this column isn't defined using DB enum only two values are used + clusterSelectionType: { + kind: 'checked', + type: PLAN_VERSION_CLUSTER_SELECTION_TYPE, + }, + }, + }, + idField: 'id', + softDeletionEnabled: false, +}); diff --git a/src/db/models/planYear.ts b/src/db/models/planYear.ts index dae22db9..b60c7095 100644 --- a/src/db/models/planYear.ts +++ b/src/db/models/planYear.ts @@ -2,6 +2,9 @@ import * as t from 'io-ts'; import { brandedType } from '../../util/io-ts'; import type { Brand } from '../../util/types'; +import { defineLegacyVersionedModel } from '../util/legacy-versioned-model'; +import { PLAN_ID } from './plan'; +import { USAGE_YEAR_ID } from './usageYear'; export type PlanYearId = Brand< number, @@ -10,3 +13,18 @@ export type PlanYearId = Brand< >; export const PLAN_YEAR_ID = brandedType(t.number); + +export default defineLegacyVersionedModel({ + tableName: 'planYear', + fields: { + generated: { + id: { kind: 'branded-integer', brand: PLAN_YEAR_ID }, + }, + required: { + planId: { kind: 'branded-integer', brand: PLAN_ID }, + usageYearId: { kind: 'branded-integer', brand: USAGE_YEAR_ID }, + }, + }, + idField: 'id', + softDeletionEnabled: true, +}); diff --git a/src/db/models/project.ts b/src/db/models/project.ts index 9f0db52d..05ecb22b 100644 --- a/src/db/models/project.ts +++ b/src/db/models/project.ts @@ -30,6 +30,14 @@ export const PROJECT_VERSION_ID = brandedType( t.number ); +const PROJECT_IMPLEMENTATION_STATUS = { + Planning: null, + Implementing: null, + 'Ended - Completed': null, + 'Ended - Terminated': null, + 'Ended - Not started and abandoned': null, +}; + const PROJECT_PDF_ENTRY = t.type({ /** * TODO: use something more stable, like UNIX OFFSET as a number @@ -60,7 +68,10 @@ export default defineIDModel({ }, optional: { code: { kind: 'checked', type: t.string }, - implementationStatus: { kind: 'checked', type: t.string }, + implementationStatus: { + kind: 'enum', + values: PROJECT_IMPLEMENTATION_STATUS, + }, currentPublishedVersionId: { kind: 'branded-integer', brand: PROJECT_VERSION_ID, diff --git a/src/db/models/projectVersionAttachment.ts b/src/db/models/projectVersionAttachment.ts index 53191c97..d43dc761 100644 --- a/src/db/models/projectVersionAttachment.ts +++ b/src/db/models/projectVersionAttachment.ts @@ -13,6 +13,8 @@ export default defineSequelizeModel({ kind: 'branded-integer', brand: ATTACHMENT_VERSION_ID, }, + }, + optional: { value: { kind: 'checked', type: t.unknown }, total: { kind: 'checked', type: t.number }, }, diff --git a/src/db/models/usageYear.ts b/src/db/models/usageYear.ts index 4defd8a8..b775ac0f 100644 --- a/src/db/models/usageYear.ts +++ b/src/db/models/usageYear.ts @@ -2,6 +2,7 @@ import * as t from 'io-ts'; import { brandedType } from '../../util/io-ts'; import type { Brand } from '../../util/types'; +import { defineIDModel } from '../util/id-model'; export type UsageYearId = Brand< number, @@ -10,3 +11,17 @@ export type UsageYearId = Brand< >; export const USAGE_YEAR_ID = brandedType(t.number); + +export default defineIDModel({ + tableName: 'usageYear', + fields: { + generated: { + id: { kind: 'branded-integer', brand: USAGE_YEAR_ID }, + }, + required: { + year: { kind: 'checked', type: t.string }, + }, + }, + idField: 'id', + softDeletionEnabled: false, +}); diff --git a/src/db/models/workflowStatusOption.ts b/src/db/models/workflowStatusOption.ts index b69edf99..5964a0e6 100644 --- a/src/db/models/workflowStatusOption.ts +++ b/src/db/models/workflowStatusOption.ts @@ -2,6 +2,7 @@ import * as t from 'io-ts'; import { brandedType } from '../../util/io-ts'; import type { Brand } from '../../util/types'; +import { LOCALIZED_STRING } from '../util/datatypes'; import { defineIDModel } from '../util/id-model'; import { PLAN_ID } from './plan'; @@ -33,9 +34,7 @@ export const WORKFLOW_STATUS_OPTION_TYPE = t.keyof({ }); export const WORKFLOW_STATUS_OPTION_VALUE = t.type({ - label: t.type({ - en: t.string, - }), + label: LOCALIZED_STRING, }); export default defineIDModel({ diff --git a/src/db/util/datatypes.ts b/src/db/util/datatypes.ts index d9771d09..e850cd95 100644 --- a/src/db/util/datatypes.ts +++ b/src/db/util/datatypes.ts @@ -49,3 +49,17 @@ export const FILE_REFERENCE = t.type({ }); export type FileReference = t.TypeOf; + +const localized = >(type: T) => + t.type({ + en: type, + }); + +export const LOCALIZED_STRING = localized(t.string); + +export const LOCALIZED_PLURAL_STRING = localized( + t.type({ + singular: t.string, + plural: t.string, + }) +); diff --git a/src/util/async.ts b/src/util/async.ts index 8731ac7d..f4a2325b 100644 --- a/src/util/async.ts +++ b/src/util/async.ts @@ -44,7 +44,9 @@ export const createGroupableAsyncFunction = < } } catch (err) { for (const call of cs) { - call.reject(err); + if (err instanceof Error) { + call.reject(err); + } } } }; diff --git a/yarn.lock b/yarn.lock index 8187a3d5..7cd58c79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2256,10 +2256,10 @@ 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== -typescript@^4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@^4.4.4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" + integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== unc-path-regex@^0.1.2: version "0.1.2"