aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main')
-rw-r--r--packages/main/package.json5
-rw-r--r--packages/main/src/controllers/initConfig.ts12
-rw-r--r--packages/main/src/stores/Config.ts32
-rw-r--r--packages/main/src/stores/Profile.ts51
-rw-r--r--packages/main/src/stores/Service.ts63
-rw-r--r--packages/main/src/stores/__tests__/Config.spec.ts156
-rw-r--r--packages/main/src/utils/generateId.ts27
7 files changed, 337 insertions, 9 deletions
diff --git a/packages/main/package.json b/packages/main/package.json
index 862b83a..80a93b9 100644
--- a/packages/main/package.json
+++ b/packages/main/package.json
@@ -20,7 +20,9 @@
20 "mobx": "^6.3.13", 20 "mobx": "^6.3.13",
21 "mobx-state-tree": "^5.1.0", 21 "mobx-state-tree": "^5.1.0",
22 "ms": "^2.1.3", 22 "ms": "^2.1.3",
23 "os-name": "^5.0.1" 23 "nanoid": "^3.1.30",
24 "os-name": "^5.0.1",
25 "slug": "^5.2.0"
24 }, 26 },
25 "devDependencies": { 27 "devDependencies": {
26 "@jest/globals": "^27.4.6", 28 "@jest/globals": "^27.4.6",
@@ -28,6 +30,7 @@
28 "@types/lodash-es": "^4.17.5", 30 "@types/lodash-es": "^4.17.5",
29 "@types/ms": "^0.7.31", 31 "@types/ms": "^0.7.31",
30 "@types/node": "^17.0.12", 32 "@types/node": "^17.0.12",
33 "@types/slug": "^5",
31 "electron-devtools-installer": "^3.2.0", 34 "electron-devtools-installer": "^3.2.0",
32 "esbuild": "^0.14.14", 35 "esbuild": "^0.14.14",
33 "git-repo-info": "^2.1.1", 36 "git-repo-info": "^2.1.1",
diff --git a/packages/main/src/controllers/initConfig.ts b/packages/main/src/controllers/initConfig.ts
index 915f451..93be978 100644
--- a/packages/main/src/controllers/initConfig.ts
+++ b/packages/main/src/controllers/initConfig.ts
@@ -19,11 +19,11 @@
19 */ 19 */
20 20
21import { debounce } from 'lodash-es'; 21import { debounce } from 'lodash-es';
22import { applySnapshot, getSnapshot, onSnapshot } from 'mobx-state-tree'; 22import { getSnapshot, onSnapshot } from 'mobx-state-tree';
23import ms from 'ms'; 23import ms from 'ms';
24 24
25import type ConfigPersistenceService from '../services/ConfigPersistenceService'; 25import type ConfigPersistenceService from '../services/ConfigPersistenceService.js';
26import type { Config, ConfigSnapshotOut } from '../stores/Config'; 26import { Config, ConfigFileIn, ConfigSnapshotOut } from '../stores/Config.js';
27import type Disposer from '../utils/Disposer'; 27import type Disposer from '../utils/Disposer';
28import { getLogger } from '../utils/log'; 28import { getLogger } from '../utils/log';
29 29
@@ -44,12 +44,14 @@ export default async function initConfig(
44 const result = await persistenceService.readConfig(); 44 const result = await persistenceService.readConfig();
45 if (result.found) { 45 if (result.found) {
46 try { 46 try {
47 applySnapshot(config, result.data); 47 // This cast is unsound if the config file is invalid,
48 lastSnapshotOnDisk = getSnapshot(config); 48 // but we'll throw an error in the end anyways.
49 config.loadFromConfigFile(result.data as ConfigFileIn);
49 } catch (error) { 50 } catch (error) {
50 log.error('Failed to apply config snapshot', result.data, error); 51 log.error('Failed to apply config snapshot', result.data, error);
51 } 52 }
52 } 53 }
54 lastSnapshotOnDisk = getSnapshot(config);
53 return result.found; 55 return result.found;
54 } 56 }
55 57
diff --git a/packages/main/src/stores/Config.ts b/packages/main/src/stores/Config.ts
index ca90c0c..e7fc360 100644
--- a/packages/main/src/stores/Config.ts
+++ b/packages/main/src/stores/Config.ts
@@ -19,14 +19,40 @@
19 */ 19 */
20 20
21import { config as originalConfig, ThemeSource } from '@sophie/shared'; 21import { config as originalConfig, ThemeSource } from '@sophie/shared';
22import { Instance } from 'mobx-state-tree'; 22import { applySnapshot, Instance, SnapshotIn } from 'mobx-state-tree';
23
24import { addMissingProfileIds, PartialProfileSnapshotIn } from './Profile';
25import {
26 addMissingServiceIdsAndProfiles,
27 PartialServiceSnapshotIn,
28} from './Service';
23 29
24export const config = originalConfig.actions((self) => ({ 30export const config = originalConfig.actions((self) => ({
25 setThemeSource(mode: ThemeSource) { 31 loadFromConfigFile(snapshot: ConfigFileIn): void {
32 const profiles = addMissingProfileIds(snapshot.profiles);
33 const services = addMissingServiceIdsAndProfiles(
34 snapshot.services,
35 profiles,
36 );
37 applySnapshot(self, {
38 ...snapshot,
39 profiles,
40 services,
41 });
42 },
43 setThemeSource(mode: ThemeSource): void {
26 self.themeSource = mode; 44 self.themeSource = mode;
27 }, 45 },
28})); 46}));
29 47
30export interface Config extends Instance<typeof config> {} 48export interface Config extends Instance<typeof config> {}
31 49
32export type { ConfigSnapshotIn, ConfigSnapshotOut } from '@sophie/shared'; 50export interface ConfigSnapshotIn extends SnapshotIn<typeof config> {}
51
52export interface ConfigFileIn
53 extends Omit<ConfigSnapshotIn, 'profiles' | 'services'> {
54 profiles?: PartialProfileSnapshotIn[] | undefined;
55 services?: PartialServiceSnapshotIn[] | undefined;
56}
57
58export type { ConfigSnapshotOut } from '@sophie/shared';
diff --git a/packages/main/src/stores/Profile.ts b/packages/main/src/stores/Profile.ts
new file mode 100644
index 0000000..4705862
--- /dev/null
+++ b/packages/main/src/stores/Profile.ts
@@ -0,0 +1,51 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import type { ProfileSnapshotIn } from '@sophie/shared';
22
23import generateId from '../utils/generateId';
24
25export interface PartialProfileSnapshotIn
26 extends Omit<ProfileSnapshotIn, 'id'> {
27 id?: string | undefined;
28}
29
30export function addMissingProfileIds(
31 partialProfiles: PartialProfileSnapshotIn[] | undefined,
32): ProfileSnapshotIn[] {
33 return (partialProfiles ?? []).map((profile) => {
34 const { name } = profile;
35 let { id } = profile;
36 if (typeof id === 'undefined') {
37 id = generateId(name);
38 }
39 return {
40 ...profile,
41 id,
42 };
43 });
44}
45
46export type {
47 Profile,
48 ProfileSnapshotOut,
49 ProfileSnapshotIn,
50} from '@sophie/shared';
51export { profile } from '@sophie/shared';
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts
new file mode 100644
index 0000000..9bc6a43
--- /dev/null
+++ b/packages/main/src/stores/Service.ts
@@ -0,0 +1,63 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import type { ServiceSnapshotIn } from '@sophie/shared';
22
23import generateId from '../utils/generateId';
24
25import type { ProfileSnapshotIn } from './Profile';
26
27export interface PartialServiceSnapshotIn
28 extends Omit<ServiceSnapshotIn, 'id' | 'profile'> {
29 id?: string | undefined;
30 profile?: string | undefined;
31}
32
33export function addMissingServiceIdsAndProfiles(
34 partialServices: PartialServiceSnapshotIn[] | undefined,
35 profiles: ProfileSnapshotIn[],
36): ServiceSnapshotIn[] {
37 return (partialServices ?? []).map((service) => {
38 const { name } = service;
39 let { id, profile } = service;
40 if (typeof id === 'undefined') {
41 id = generateId(name);
42 }
43 if (typeof profile === 'undefined') {
44 profile = generateId(name);
45 profiles.push({
46 id: profile,
47 name: service.name,
48 });
49 }
50 return {
51 ...service,
52 id,
53 profile,
54 };
55 });
56}
57
58export type {
59 Service,
60 ServiceSnapshotOut,
61 ServiceSnapshotIn,
62} from '@sophie/shared';
63export { service } from '@sophie/shared';
diff --git a/packages/main/src/stores/__tests__/Config.spec.ts b/packages/main/src/stores/__tests__/Config.spec.ts
new file mode 100644
index 0000000..22ccbc7
--- /dev/null
+++ b/packages/main/src/stores/__tests__/Config.spec.ts
@@ -0,0 +1,156 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import { config, Config, ConfigFileIn } from '../Config';
22import type { PartialProfileSnapshotIn } from '../Profile';
23import type { PartialServiceSnapshotIn } from '../Service';
24
25const profileProps: PartialProfileSnapshotIn = {
26 name: 'Test profile',
27};
28
29const serviceProps: PartialServiceSnapshotIn = {
30 name: 'Test service',
31 url: 'https://example.com',
32};
33
34let sut: Config;
35
36beforeEach(() => {
37 sut = config.create();
38});
39
40describe('preprocessConfigFile', () => {
41 it('should load profiles with an ID', () => {
42 sut.loadFromConfigFile({
43 profiles: [
44 {
45 id: 'someId',
46 ...profileProps,
47 },
48 ],
49 });
50 expect(sut.profiles[0].id).toBe('someId');
51 });
52
53 it('should generate an ID for profiles without and ID', () => {
54 sut.loadFromConfigFile({
55 profiles: [profileProps],
56 });
57 expect(sut.profiles[0].id).toBeDefined();
58 });
59
60 it('should load services with an ID and a profile', () => {
61 sut.loadFromConfigFile({
62 profiles: [
63 {
64 id: 'someProfileId',
65 ...profileProps,
66 },
67 ],
68 services: [
69 {
70 id: 'someServiceId',
71 profile: 'someProfileId',
72 ...serviceProps,
73 },
74 ],
75 });
76 expect(sut.services[0].id).toBe('someServiceId');
77 expect(sut.services[0].profile).toBe(sut.profiles[0]);
78 });
79
80 it('should refuse to load a profile without a name', () => {
81 expect(() => {
82 sut.loadFromConfigFile({
83 profiles: [
84 {
85 id: 'someProfileId',
86 ...profileProps,
87 name: undefined,
88 },
89 ],
90 } as unknown as ConfigFileIn);
91 }).toThrow();
92 expect(sut.profiles).toHaveLength(0);
93 });
94
95 it('should load services without an ID but with a profile', () => {
96 sut.loadFromConfigFile({
97 profiles: [
98 {
99 id: 'someProfileId',
100 ...profileProps,
101 },
102 ],
103 services: [
104 {
105 profile: 'someProfileId',
106 ...serviceProps,
107 },
108 ],
109 });
110 expect(sut.services[0].id).toBeDefined();
111 expect(sut.services[0].profile).toBe(sut.profiles[0]);
112 });
113
114 it('should create a profile for a service with an ID but no profile', () => {
115 sut.loadFromConfigFile({
116 services: [
117 {
118 id: 'someServiceId',
119 ...serviceProps,
120 },
121 ],
122 });
123 expect(sut.services[0].id).toBe('someServiceId');
124 expect(sut.services[0].profile).toBeDefined();
125 expect(sut.services[0].profile.name).toBe(serviceProps.name);
126 });
127
128 it('should create a profile for a service without an ID or profile', () => {
129 sut.loadFromConfigFile({
130 services: [
131 {
132 ...serviceProps,
133 },
134 ],
135 });
136 expect(sut.services[0].id).toBeDefined();
137 expect(sut.services[0].profile).toBeDefined();
138 expect(sut.services[0].profile.name).toBe(serviceProps.name);
139 });
140
141 it('should refuse to load a service without a name', () => {
142 expect(() => {
143 sut.loadFromConfigFile({
144 services: [
145 {
146 id: 'someServiceId',
147 ...serviceProps,
148 name: undefined,
149 },
150 ],
151 } as unknown as ConfigFileIn);
152 }).toThrow();
153 expect(sut.profiles).toHaveLength(0);
154 expect(sut.services).toHaveLength(0);
155 });
156});
diff --git a/packages/main/src/utils/generateId.ts b/packages/main/src/utils/generateId.ts
new file mode 100644
index 0000000..8a87e5a
--- /dev/null
+++ b/packages/main/src/utils/generateId.ts
@@ -0,0 +1,27 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import { nanoid } from 'nanoid';
22import slug from 'slug';
23
24export default function generateId(name?: string | undefined) {
25 const nameSlug = typeof name === 'undefined' ? '' : slug(name);
26 return `${nameSlug}_${nanoid()}`;
27}