diff --git a/src/action/addusedtime2.ts b/src/action/addusedtime2.ts new file mode 100644 index 0000000..9c456f5 --- /dev/null +++ b/src/action/addusedtime2.ts @@ -0,0 +1,98 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { uniq } from 'lodash' +import { assertIdWithinFamily } from '../util/token' +import { AppLogicAction } from './basetypes' + +export class AddUsedTimeActionVersion2 extends AppLogicAction { + readonly dayOfEpoch: number + readonly items: Array<{ + readonly categoryId: string + readonly timeToAdd: number + readonly extraTimeToSubtract: number + }> + + constructor ({ dayOfEpoch, items }: { + dayOfEpoch: number + items: Array<{ + categoryId: string + timeToAdd: number + extraTimeToSubtract: number + }> + }) { + super() + + if (dayOfEpoch < 0 || (!Number.isSafeInteger(dayOfEpoch))) { + throw new Error('illegal dayOfEpoch') + } + + if (items.length === 0) { + throw new Error('missing items') + } + + if (items.length !== uniq(items.map((item) => item.categoryId)).length) { + throw new Error('duplicate category ids') + } + + items.forEach((item) => { + assertIdWithinFamily(item.categoryId) + + if (item.timeToAdd < 0 || (!Number.isSafeInteger(item.timeToAdd))) { + throw new Error('illegal timeToAdd') + } + + if (item.extraTimeToSubtract < 0 || (!Number.isSafeInteger(item.extraTimeToSubtract))) { + throw new Error('illegal extra time to subtract') + } + }) + + this.dayOfEpoch = dayOfEpoch + this.items = items + } + + serialize = (): SerializedAddUsedTimeActionVersion2 => ({ + type: 'ADD_USED_TIME_V2', + d: this.dayOfEpoch, + i: this.items.map((item) => ({ + categoryId: item.categoryId, + tta: item.timeToAdd, + etts: item.extraTimeToSubtract + })) + }) + + static parse = ({ d, i }: SerializedAddUsedTimeActionVersion2) => ( + new AddUsedTimeActionVersion2({ + dayOfEpoch: d, + items: i.map((item) => ({ + categoryId: item.categoryId, + timeToAdd: item.tta, + extraTimeToSubtract: item.etts + })) + }) + ) +} + +export interface SerializedAddUsedTimeActionVersion2 { + type: 'ADD_USED_TIME_V2' + d: number + i: Array<{ + categoryId: string + tta: number + etts: number + }> +} diff --git a/src/action/index.ts b/src/action/index.ts index 3a0e707..def6420 100644 --- a/src/action/index.ts +++ b/src/action/index.ts @@ -21,6 +21,7 @@ export { AddCategoryAppsAction } from './addcategoryapps' export { AddUserAction } from './adduser' export { AddInstalledAppsAction } from './addinstalledapps' export { AddUsedTimeAction } from './addusedtime' +export { AddUsedTimeActionVersion2 } from './addusedtime2' export { ChangeParentPasswordAction } from './changeparentpassword' export { ChildChangePasswordAction } from './childchangepassword' export { ChildSignInAction } from './childsignin' diff --git a/src/action/serialization/applogicaction.ts b/src/action/serialization/applogicaction.ts index 0a514af..eb87bf0 100644 --- a/src/action/serialization/applogicaction.ts +++ b/src/action/serialization/applogicaction.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -17,6 +17,7 @@ import { AddInstalledAppsAction, SerializedAddInstalledAppsAction } from '../addinstalledapps' import { AddUsedTimeAction, SerializedAddUsedTimeAction } from '../addusedtime' +import { AddUsedTimeActionVersion2, SerializedAddUsedTimeActionVersion2 } from '../addusedtime2' import { AppLogicAction } from '../basetypes' import { RemoveInstalledAppsAction, SerializedRemoveInstalledAppsAction } from '../removeinstalledapps' import { SerializedSignOutAtDeviceAction, SignOutAtDeviceAction } from '../signoutatdevice' @@ -27,6 +28,7 @@ import { SerializedUpdateDeviceStatusAction, UpdateDeviceStatusAction } from '.. export type SerializedAppLogicAction = SerializedAddInstalledAppsAction | SerializedAddUsedTimeAction | + SerializedAddUsedTimeActionVersion2 | SerializedRemoveInstalledAppsAction | SerializedSignOutAtDeviceAction | SerialiezdTriedDisablingDeviceAdminAction | @@ -36,6 +38,8 @@ export type SerializedAppLogicAction = export const parseAppLogicAction = (serialized: SerializedAppLogicAction): AppLogicAction => { if (serialized.type === 'ADD_USED_TIME') { return AddUsedTimeAction.parse(serialized) + } else if (serialized.type === 'ADD_USED_TIME_V2') { + return AddUsedTimeActionVersion2.parse(serialized) } else if (serialized.type === 'ADD_INSTALLED_APPS') { return AddInstalledAppsAction.parse(serialized) } else if (serialized.type === 'REMOVE_INSTALLED_APPS') { diff --git a/src/api/validator.ts b/src/api/validator.ts index 7959c61..354258a 100644 --- a/src/api/validator.ts +++ b/src/api/validator.ts @@ -1152,6 +1152,49 @@ const definitions = { "type" ] }, + "SerializedAddUsedTimeActionVersion2": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ADD_USED_TIME_V2" + ] + }, + "d": { + "type": "number" + }, + "i": { + "type": "array", + "items": { + "type": "object", + "properties": { + "categoryId": { + "type": "string" + }, + "tta": { + "type": "number" + }, + "etts": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "categoryId", + "etts", + "tta" + ] + } + } + }, + "additionalProperties": false, + "required": [ + "d", + "i", + "type" + ] + }, "SerializedRemoveInstalledAppsAction": { "type": "object", "properties": { @@ -1692,6 +1735,9 @@ export const isSerializedAppLogicAction: (value: object) => value is SerializedA { "$ref": "#/definitions/SerializedAddUsedTimeAction" }, + { + "$ref": "#/definitions/SerializedAddUsedTimeActionVersion2" + }, { "$ref": "#/definitions/SerializedRemoveInstalledAppsAction" }, diff --git a/src/function/sync/apply-actions/cache.ts b/src/function/sync/apply-actions/cache.ts index a38317d..f9ae4d9 100644 --- a/src/function/sync/apply-actions/cache.ts +++ b/src/function/sync/apply-actions/cache.ts @@ -120,7 +120,7 @@ export class Cache { }) shouldDoFullSync = () => this.shouldTriggerFullSync - requireFullSync = () => this.shouldTriggerFullSync = true + requireFullSync: () => void = () => this.shouldTriggerFullSync = true async saveModifiedVersionNumbers () { const { database, transaction, familyId } = this diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime.ts index bf8cd7f..a259f21 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime.ts @@ -19,7 +19,7 @@ import * as Sequelize from 'sequelize' import { AddUsedTimeAction } from '../../../../action' import { Cache } from '../cache' -const getRoundedTimestamp = () => { +export const getRoundedTimestamp = () => { const now = Date.now() return now - (now % (1000 * 60 * 60 * 24 * 2 /* 2 days */)) diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime2.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime2.ts new file mode 100644 index 0000000..04d8742 --- /dev/null +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime2.ts @@ -0,0 +1,100 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import * as Sequelize from 'sequelize' +import { AddUsedTimeActionVersion2 } from '../../../../action' +import { Cache } from '../cache' +import { getRoundedTimestamp } from './addusedtime' + +export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache }: { + deviceId: string + action: AddUsedTimeActionVersion2 + cache: Cache +}) { + const roundedTimestamp = getRoundedTimestamp().toString(10) + + for (let i = 0; i < action.items.length; i++) { + const item = action.items[i] + + const categoryEntryUnsafe = await cache.database.category.findOne({ + where: { + familyId: cache.familyId, + categoryId: item.categoryId + }, + transaction: cache.transaction, + attributes: [ + 'childId', + 'extraTimeInMillis' + ] + }) + // verify that the category exists + if (!categoryEntryUnsafe) { + cache.requireFullSync() + + return + } + + const categoryEntry = { + childId: categoryEntryUnsafe.childId, + extraTimeInMillis: categoryEntryUnsafe.extraTimeInMillis + } + + if (item.timeToAdd !== 0) { + // try to update first + const [updatedRows] = await cache.database.usedTime.update({ + usedTime: Sequelize.literal(`usedTime + ${item.timeToAdd}`) as any, + lastUpdate: roundedTimestamp + }, { + where: { + familyId: cache.familyId, + categoryId: item.categoryId, + dayOfEpoch: action.dayOfEpoch + }, + transaction: cache.transaction + }) + + // otherwise create + if (updatedRows === 0) { + await cache.database.usedTime.create({ + familyId: cache.familyId, + categoryId: item.categoryId, + dayOfEpoch: action.dayOfEpoch, + usedTime: item.timeToAdd, + lastUpdate: roundedTimestamp + }, { + transaction: cache.transaction + }) + } + + cache.categoriesWithModifiedUsedTimes.push(item.categoryId) + } + + if (item.extraTimeToSubtract !== 0) { + await cache.database.category.update({ + extraTimeInMillis: Math.max(0, categoryEntry.extraTimeInMillis - item.extraTimeToSubtract) + }, { + where: { + familyId: cache.familyId, + categoryId: item.categoryId + }, + transaction: cache.transaction + }) + + cache.categoriesWithModifiedBaseData.push(item.categoryId) + } + } +} diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/index.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/index.ts index 1e7c64a..f91deb3 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/index.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/index.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,6 +18,7 @@ import { AddInstalledAppsAction, AddUsedTimeAction, + AddUsedTimeActionVersion2, AppLogicAction, RemoveInstalledAppsAction, SignOutAtDeviceAction, @@ -28,6 +29,7 @@ import { import { Cache } from '../cache' import { dispatchAddInstalledApps } from './addinstalledapps' import { dispatchAddUsedTime } from './addusedtime' +import { dispatchAddUsedTimeVersion2 } from './addusedtime2' import { dispatchRemoveInstalledApps } from './removeinstalledapps' import { dispatchSignOutAtDevice } from './signoutatdevice' import { dispatchTriedDisablingDeviceAdmin } from './trieddisablingdeviceadmin' @@ -43,6 +45,8 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache }: { await dispatchAddInstalledApps({ deviceId, action, cache }) } else if (action instanceof AddUsedTimeAction) { await dispatchAddUsedTime({ deviceId, action, cache }) + } else if (action instanceof AddUsedTimeActionVersion2) { + await dispatchAddUsedTimeVersion2({ deviceId, action, cache }) } else if (action instanceof RemoveInstalledAppsAction) { await dispatchRemoveInstalledApps({ deviceId, action, cache }) } else if (action instanceof SignOutAtDeviceAction) {