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) {