-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
migration-helper.ts
232 lines (211 loc) · 8.36 KB
/
migration-helper.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
import { LogService } from "../platform/abstractions/log.service";
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
import { AbstractStorageService } from "../platform/abstractions/storage.service";
export type StateDefinitionLike = { name: string };
export type KeyDefinitionLike = {
stateDefinition: StateDefinitionLike;
key: string;
};
export type MigrationHelperType = "general" | "web-disk-local";
export class MigrationHelper {
constructor(
public currentVersion: number,
private storageService: AbstractStorageService,
public logService: LogService,
type: MigrationHelperType,
) {
this.type = type;
}
/**
* On some clients, migrations are ran multiple times without direct action from the migration writer.
*
* All clients will run through migrations at least once, this run is referred to as `"general"`. If a migration is
* ran more than that single time, they will get a unique name if that the write can make conditional logic based on which
* migration run this is.
*
* @remarks The preferrable way of writing migrations is ALWAYS to be defensive and reflect on the data you are given back. This
* should really only be used when reflecting on the data given isn't enough.
*/
type: MigrationHelperType;
/**
* Gets a value from the storage service at the given key.
*
* This is a brute force method to just get a value from the storage service. If you can use {@link getFromGlobal} or {@link getFromUser}, you should.
* @param key location
* @returns the value at the location
*/
get<T>(key: string): Promise<T> {
return this.storageService.get<T>(key);
}
/**
* Sets a value in the storage service at the given key.
*
* This is a brute force method to just set a value in the storage service. If you can use {@link setToGlobal} or {@link setToUser}, you should.
* @param key location
* @param value the value to set
* @returns
*/
set<T>(key: string, value: T): Promise<void> {
this.logService.info(`Setting ${key}`);
return this.storageService.save(key, value);
}
/**
* Remove a value in the storage service at the given key.
*
* This is a brute force method to just remove a value in the storage service. If you can use {@link removeFromGlobal} or {@link removeFromUser}, you should.
* @param key location
* @returns void
*/
remove(key: string): Promise<void> {
this.logService.info(`Removing ${key}`);
return this.storageService.remove(key);
}
/**
* Gets a globally scoped value from a location derived through the key definition
*
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
* use {@link get} for those.
* @param keyDefinition unique key definition
* @returns value from store
*/
getFromGlobal<T>(keyDefinition: KeyDefinitionLike): Promise<T> {
return this.get<T>(this.getGlobalKey(keyDefinition));
}
/**
* Sets a globally scoped value to a location derived through the key definition
*
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
* use {@link set} for those.
* @param keyDefinition unique key definition
* @param value value to store
* @returns void
*/
setToGlobal<T>(keyDefinition: KeyDefinitionLike, value: T): Promise<void> {
return this.set(this.getGlobalKey(keyDefinition), value);
}
/**
* Remove a globally scoped location derived through the key definition
*
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
* use {@link remove} for those.
* @param keyDefinition unique key definition
* @returns void
*/
removeFromGlobal(keyDefinition: KeyDefinitionLike): Promise<void> {
return this.remove(this.getGlobalKey(keyDefinition));
}
/**
* Gets a user scoped value from a location derived through the user id and key definition
*
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
* use {@link get} for those.
* @param userId userId to use in the key
* @param keyDefinition unique key definition
* @returns value from store
*/
getFromUser<T>(userId: string, keyDefinition: KeyDefinitionLike): Promise<T> {
return this.get<T>(this.getUserKey(userId, keyDefinition));
}
/**
* Sets a user scoped value to a location derived through the user id and key definition
*
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
* use {@link set} for those.
* @param userId userId to use in the key
* @param keyDefinition unique key definition
* @param value value to store
* @returns void
*/
setToUser<T>(userId: string, keyDefinition: KeyDefinitionLike, value: T): Promise<void> {
return this.set(this.getUserKey(userId, keyDefinition), value);
}
/**
* Remove a user scoped location derived through the key definition
*
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
* use {@link remove} for those.
* @param keyDefinition unique key definition
* @returns void
*/
removeFromUser(userId: string, keyDefinition: KeyDefinitionLike): Promise<void> {
return this.remove(this.getUserKey(userId, keyDefinition));
}
info(message: string): void {
this.logService.info(message);
}
/**
* Helper method to read all Account objects stored by the State Service.
*
* This is useful from creating migrations off of this paradigm, but should not be used once a value is migrated to a state provider.
*
* @returns a list of all accounts that have been authenticated with state service, cast the expected type.
*/
async getAccounts<ExpectedAccountType>(): Promise<
{ userId: string; account: ExpectedAccountType }[]
> {
const userIds = (await this.get<string[]>("authenticatedAccounts")) ?? [];
return Promise.all(
userIds.map(async (userId) => ({
userId,
account: await this.get<ExpectedAccountType>(userId),
})),
);
}
/**
* Builds a user storage key appropriate for the current version.
*
* @param userId userId to use in the key
* @param keyDefinition state and key to use in the key
* @returns
*/
private getUserKey(userId: string, keyDefinition: KeyDefinitionLike): string {
if (this.currentVersion < 9) {
return userKeyBuilderPre9();
} else {
return userKeyBuilder(userId, keyDefinition);
}
}
/**
* Builds a global storage key appropriate for the current version.
*
* @param keyDefinition state and key to use in the key
* @returns
*/
private getGlobalKey(keyDefinition: KeyDefinitionLike): string {
if (this.currentVersion < 9) {
return globalKeyBuilderPre9();
} else {
return globalKeyBuilder(keyDefinition);
}
}
}
/**
* When this is updated, rename this function to `userKeyBuilderXToY` where `X` is the version number it
* became relevant, and `Y` prior to the version it was updated.
*
* Be sure to update the map in `MigrationHelper` to point to the appropriate function for the current version.
* @param userId The userId of the user you want the key to be for.
* @param keyDefinition the key definition of which data the key should point to.
* @returns
*/
function userKeyBuilder(userId: string, keyDefinition: KeyDefinitionLike): string {
return `user_${userId}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
}
function userKeyBuilderPre9(): string {
throw Error("No key builder should be used for versions prior to 9.");
}
/**
* When this is updated, rename this function to `globalKeyBuilderXToY` where `X` is the version number
* it became relevant, and `Y` prior to the version it was updated.
*
* Be sure to update the map in `MigrationHelper` to point to the appropriate function for the current version.
* @param keyDefinition the key definition of which data the key should point to.
* @returns
*/
function globalKeyBuilder(keyDefinition: KeyDefinitionLike): string {
return `global_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
}
function globalKeyBuilderPre9(): string {
throw Error("No key builder should be used for versions prior to 9.");
}