@balena/pinejs 14.44.0-linear-runtime-migrator-e15bea04e85fb013aeed7d4770052b51747a619c → 15.0.0-delete-state-default-user-permissions-ba0732a0c5d0da9d1d5be818cb08cc898e86ebe3
Sign up to get free protection for your applications and to get access to all the features.
- package/.versionbot/CHANGELOG.yml +24 -45
- package/CHANGELOG.md +8 -6
- package/VERSION +1 -1
- package/docs/Migrations.md +1 -101
- package/out/config-loader/config-loader.d.ts +4 -2
- package/out/config-loader/config-loader.js +20 -35
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.d.ts +0 -3
- package/out/config-loader/env.js +0 -3
- package/out/config-loader/env.js.map +1 -1
- package/out/migrator/migrations.sbvr +0 -66
- package/out/migrator/migrator.d.ts +17 -0
- package/out/migrator/migrator.js +185 -0
- package/out/migrator/migrator.js.map +1 -0
- package/out/sbvr-api/sbvr-utils.d.ts +1 -3
- package/out/sbvr-api/sbvr-utils.js +4 -13
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/server-glue/module.d.ts +2 -2
- package/out/server-glue/module.js +1 -2
- package/out/server-glue/module.js.map +1 -1
- package/package.json +3 -3
- package/src/config-loader/config-loader.ts +26 -73
- package/src/config-loader/env.ts +0 -3
- package/src/migrator/migrations.sbvr +0 -66
- package/src/migrator/migrator.ts +278 -0
- package/src/sbvr-api/sbvr-utils.ts +3 -18
- package/src/server-glue/module.ts +2 -3
- package/out/migrator/async.d.ts +0 -6
- package/out/migrator/async.js +0 -160
- package/out/migrator/async.js.map +0 -1
- package/out/migrator/sync.d.ts +0 -9
- package/out/migrator/sync.js +0 -126
- package/out/migrator/sync.js.map +0 -1
- package/out/migrator/utils.d.ts +0 -56
- package/out/migrator/utils.js +0 -187
- package/out/migrator/utils.js.map +0 -1
- package/src/migrator/async.ts +0 -279
- package/src/migrator/sync.ts +0 -177
- package/src/migrator/utils.ts +0 -296
package/src/migrator/async.ts
DELETED
@@ -1,279 +0,0 @@
|
|
1
|
-
import type { Tx } from '../database-layer/db';
|
2
|
-
import type { Model } from '../config-loader/config-loader';
|
3
|
-
|
4
|
-
import * as _ from 'lodash';
|
5
|
-
import * as sbvrUtils from '../sbvr-api/sbvr-utils';
|
6
|
-
|
7
|
-
type ApiRootModel = Model & { apiRoot: string };
|
8
|
-
|
9
|
-
import {
|
10
|
-
MigrationTuple,
|
11
|
-
AsyncMigrationFn,
|
12
|
-
MigrationCategories,
|
13
|
-
checkModelAlreadyExists,
|
14
|
-
setExecutedMigrations,
|
15
|
-
getExecutedMigrations,
|
16
|
-
migratorEnv,
|
17
|
-
lockMigrations,
|
18
|
-
initMigrationStatus,
|
19
|
-
readMigrationStatus,
|
20
|
-
updateMigrationStatus,
|
21
|
-
RunnableMigrations,
|
22
|
-
} from './utils';
|
23
|
-
|
24
|
-
export const run = async (model: ApiRootModel): Promise<void> => {
|
25
|
-
const { migrations } = model;
|
26
|
-
if (
|
27
|
-
migrations == null ||
|
28
|
-
_.isEmpty(migrations) ||
|
29
|
-
migrations[MigrationCategories.async] === undefined
|
30
|
-
) {
|
31
|
-
return;
|
32
|
-
}
|
33
|
-
let runMigrations: RunnableMigrations = {};
|
34
|
-
for (const [key, value] of Object.entries(migrations)) {
|
35
|
-
if (key === MigrationCategories.async && value instanceof Object) {
|
36
|
-
runMigrations = { ...runMigrations, ...value };
|
37
|
-
}
|
38
|
-
}
|
39
|
-
await $run(model, runMigrations);
|
40
|
-
};
|
41
|
-
|
42
|
-
const $run = async (
|
43
|
-
model: ApiRootModel,
|
44
|
-
migrations: RunnableMigrations,
|
45
|
-
): Promise<void> => {
|
46
|
-
const modelName = model.apiRoot;
|
47
|
-
|
48
|
-
// init migrations
|
49
|
-
let exists;
|
50
|
-
await sbvrUtils.db.transaction(async (tx) => {
|
51
|
-
exists = await checkModelAlreadyExists(tx, modelName);
|
52
|
-
if (!exists) {
|
53
|
-
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
54
|
-
'First time model has executed, skipping migrations',
|
55
|
-
);
|
56
|
-
|
57
|
-
return await setExecutedMigrations(
|
58
|
-
tx,
|
59
|
-
modelName,
|
60
|
-
Object.keys(migrations),
|
61
|
-
);
|
62
|
-
}
|
63
|
-
});
|
64
|
-
|
65
|
-
if (!exists) {
|
66
|
-
return;
|
67
|
-
}
|
68
|
-
|
69
|
-
/**
|
70
|
-
* preflight check if there are already migrations executed before starting the async scheduler
|
71
|
-
* this will impplicitly skip async migrations that have been superceeded by synchron migrations.
|
72
|
-
* e.g.:
|
73
|
-
*
|
74
|
-
* sync migrations in repo: [001,002,004,005]
|
75
|
-
* async migrations in repo: [003,006]
|
76
|
-
*
|
77
|
-
* executed migrations at this point should always contain all sync migrations:
|
78
|
-
* executed migrations: [001,002,004,005]
|
79
|
-
*
|
80
|
-
* This will result in only async migration 006 being executed.
|
81
|
-
*
|
82
|
-
* The async migrations are ment to be used in seperate deployments to make expensive data migrations
|
83
|
-
* of multiple million row update queries cheaper and with no downtime / long lasting table lock.
|
84
|
-
* In the end, after each async migration, the next deploymemnt should follow up the data migration
|
85
|
-
* with a final sync data migrations.
|
86
|
-
* A async migration will be executed in iterations until no rows are updated anymore (keep row locks short)
|
87
|
-
* then it switches into a backoff mode to check with longer delay if data needs to be migrated in the future.
|
88
|
-
* Example query:
|
89
|
-
* UPDATE tableA
|
90
|
-
* SET columnB = columnA
|
91
|
-
* WHERE id IN (SELECT id
|
92
|
-
* FROM tableA
|
93
|
-
* WHERE (columnA <> columnB) OR (columnA IS NOT NULL AND columnB IS NULL)
|
94
|
-
* LIMIT 1000);
|
95
|
-
*
|
96
|
-
* The final sync data migration would look like:
|
97
|
-
* UPDATE tableA
|
98
|
-
* SET columnB = columnA
|
99
|
-
* WHERE (columnA <> columnB) OR (columnA IS NOT NULL AND columnB IS NULL);
|
100
|
-
*
|
101
|
-
* And will update remaining rows, which ideally are 0 and therefore now rows are locked for the update
|
102
|
-
*
|
103
|
-
* In the case of a column rename the columnA could be savely dropped:
|
104
|
-
* ALTER TABLE tableA
|
105
|
-
* DROP COLUMN IF EXISTS columnA;
|
106
|
-
*/
|
107
|
-
|
108
|
-
let pendingMigrations: MigrationTuple[] = [];
|
109
|
-
await sbvrUtils.db.transaction(async (tx) => {
|
110
|
-
const executedMigrations = await getExecutedMigrations(tx, modelName);
|
111
|
-
pendingMigrations = filterAndSortPendingAsyncMigrations(
|
112
|
-
migrations,
|
113
|
-
executedMigrations,
|
114
|
-
);
|
115
|
-
});
|
116
|
-
|
117
|
-
// Just schedule the migration workers and don't wait for any return of them
|
118
|
-
// the migration workers run until the next deployment and may synchronise with other
|
119
|
-
// instances via database tables: migration lock and migration status
|
120
|
-
for (const [key, migration] of pendingMigrations) {
|
121
|
-
const initMigrationState = {
|
122
|
-
migration_key: key,
|
123
|
-
start_time: new Date(Date.now()),
|
124
|
-
last_run_time: new Date(Date.now()),
|
125
|
-
run_counter: 0,
|
126
|
-
migrated_rows: 0,
|
127
|
-
error_counter: 0,
|
128
|
-
error_threshold: migratorEnv.asyncMigrationDefaultErrorThreshold,
|
129
|
-
delayMS: migratorEnv.asyncMigrationDefaultDelayMS,
|
130
|
-
backoffDelayMS: migratorEnv.asyncMigrationDefaultBackoffDelayMS,
|
131
|
-
converged_time: undefined,
|
132
|
-
last_error_message: undefined,
|
133
|
-
is_backoff: false,
|
134
|
-
should_stop: false,
|
135
|
-
};
|
136
|
-
|
137
|
-
let asyncRunnerMigratorFn: ((tx: Tx) => Promise<number>) | undefined;
|
138
|
-
|
139
|
-
if (typeof migration === 'object') {
|
140
|
-
if (migration.fn && typeof migration.fn === 'function') {
|
141
|
-
asyncRunnerMigratorFn = async (tx: Tx) =>
|
142
|
-
(await (migration.fn as AsyncMigrationFn)(tx, sbvrUtils))
|
143
|
-
.rowsAffected;
|
144
|
-
} else if (migration.sql && typeof migration.sql === 'string') {
|
145
|
-
asyncRunnerMigratorFn = async (tx: Tx) =>
|
146
|
-
(await tx.executeSql(migration.sql as string)).rowsAffected;
|
147
|
-
} else {
|
148
|
-
// don't break the async migration b/c of one migration fails
|
149
|
-
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
150
|
-
`Invalid migration object: ${JSON.stringify(migration, null, 2)}`,
|
151
|
-
);
|
152
|
-
continue;
|
153
|
-
}
|
154
|
-
|
155
|
-
initMigrationState.backoffDelayMS =
|
156
|
-
migration.backoffDelayMS ||
|
157
|
-
migratorEnv.asyncMigrationDefaultBackoffDelayMS;
|
158
|
-
initMigrationState.delayMS =
|
159
|
-
migration.delayMS || migratorEnv.asyncMigrationDefaultDelayMS;
|
160
|
-
initMigrationState.error_threshold =
|
161
|
-
migration.errorThreshold ||
|
162
|
-
migratorEnv.asyncMigrationDefaultErrorThreshold;
|
163
|
-
} else if (typeof migration === 'string') {
|
164
|
-
asyncRunnerMigratorFn = async (tx: Tx) =>
|
165
|
-
(await tx.executeSql(migration)).rowsAffected;
|
166
|
-
} else {
|
167
|
-
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
168
|
-
`Invalid async migration object: ${JSON.stringify(migration, null, 2)}`,
|
169
|
-
);
|
170
|
-
continue;
|
171
|
-
}
|
172
|
-
|
173
|
-
await sbvrUtils.db.transaction(async (tx) =>
|
174
|
-
initMigrationStatus(tx, initMigrationState),
|
175
|
-
);
|
176
|
-
|
177
|
-
const asyncRunner = async () => {
|
178
|
-
await sbvrUtils.db.transaction(async (tx) => {
|
179
|
-
await lockMigrations(tx, modelName, async () => {
|
180
|
-
const migrationState = await readMigrationStatus(tx, key);
|
181
|
-
|
182
|
-
if (!migrationState || migrationState.should_stop === true) {
|
183
|
-
// migration status is unclear stop the migrator
|
184
|
-
// or migration should stop
|
185
|
-
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
186
|
-
`stopping async migration: ${key}`,
|
187
|
-
);
|
188
|
-
clearInterval(asyncScheduler);
|
189
|
-
return;
|
190
|
-
}
|
191
|
-
try {
|
192
|
-
// clear the interval to avoid retriggering before finisheing this migraiton query
|
193
|
-
clearInterval(asyncScheduler);
|
194
|
-
|
195
|
-
// sync on the last execution time between instances
|
196
|
-
// precondition: All running instances are running on the same time/block
|
197
|
-
// skip execution
|
198
|
-
if (migrationState.last_run_time) {
|
199
|
-
const durationSinceLastRun =
|
200
|
-
Date.now().valueOf() - migrationState.last_run_time.valueOf();
|
201
|
-
if (
|
202
|
-
(migrationState.is_backoff &&
|
203
|
-
durationSinceLastRun < migrationState.backoffDelayMS) ||
|
204
|
-
(!migrationState.is_backoff &&
|
205
|
-
durationSinceLastRun < migrationState.delayMS)
|
206
|
-
) {
|
207
|
-
// will still execute finally block where the migration lock is released.
|
208
|
-
return;
|
209
|
-
}
|
210
|
-
}
|
211
|
-
// set last run time and run counter only when backoff time sync between
|
212
|
-
// competing instances is in sync
|
213
|
-
migrationState.last_run_time = new Date(Date.now());
|
214
|
-
migrationState.run_counter += 1;
|
215
|
-
|
216
|
-
let migratedRows = 0;
|
217
|
-
await sbvrUtils.db.transaction(async (migrationTx) => {
|
218
|
-
migratedRows = (await asyncRunnerMigratorFn?.(migrationTx)) || 0;
|
219
|
-
});
|
220
|
-
|
221
|
-
migrationState.migrated_rows += migratedRows;
|
222
|
-
if (migratedRows === 0) {
|
223
|
-
// when all rows have been catched up once we only catch up less frequently
|
224
|
-
migrationState.is_backoff = true;
|
225
|
-
if (!migrationState.converged_time) {
|
226
|
-
// only store the first time when migrator converged to all data migrated
|
227
|
-
migrationState.converged_time = new Date(Date.now());
|
228
|
-
}
|
229
|
-
} else {
|
230
|
-
// Only here for the case that after backoff more rows need to be catched up faster
|
231
|
-
// If rows have been updated recently we start the interval again with normal frequency
|
232
|
-
migrationState.is_backoff = false;
|
233
|
-
}
|
234
|
-
} catch (err) {
|
235
|
-
migrationState.error_counter++;
|
236
|
-
|
237
|
-
if (
|
238
|
-
migrationState.error_counter % migrationState.error_threshold ===
|
239
|
-
0
|
240
|
-
) {
|
241
|
-
migrationState.last_error_message = `${err.name} ${err.message}`;
|
242
|
-
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
243
|
-
`${key}: ${err.name} ${err.message}`,
|
244
|
-
);
|
245
|
-
migrationState.is_backoff = true;
|
246
|
-
}
|
247
|
-
} finally {
|
248
|
-
if (migrationState.is_backoff) {
|
249
|
-
asyncScheduler = setInterval(
|
250
|
-
asyncRunner,
|
251
|
-
migrationState.backoffDelayMS,
|
252
|
-
);
|
253
|
-
} else {
|
254
|
-
asyncScheduler = setInterval(asyncRunner, migrationState.delayMS);
|
255
|
-
}
|
256
|
-
// using finally as it will also run when return statement is called inside the try block
|
257
|
-
// either success or error release the lock
|
258
|
-
await updateMigrationStatus(tx, migrationState);
|
259
|
-
}
|
260
|
-
});
|
261
|
-
});
|
262
|
-
};
|
263
|
-
let asyncScheduler = setInterval(asyncRunner, initMigrationState.delayMS);
|
264
|
-
}
|
265
|
-
};
|
266
|
-
|
267
|
-
const filterAndSortPendingAsyncMigrations = (
|
268
|
-
migrations: NonNullable<RunnableMigrations>,
|
269
|
-
executedMigrations: string[],
|
270
|
-
): MigrationTuple[] => {
|
271
|
-
// using it for string comparison, any string is greater than ''
|
272
|
-
const latestExecutedMigration = executedMigrations.sort().pop() ?? '';
|
273
|
-
|
274
|
-
return (_(migrations).omit(executedMigrations) as _.Object<typeof migrations>)
|
275
|
-
.toPairs()
|
276
|
-
.filter(([migrationKey]) => migrationKey > latestExecutedMigration)
|
277
|
-
.sortBy(([migrationKey]) => migrationKey)
|
278
|
-
.value();
|
279
|
-
};
|
package/src/migrator/sync.ts
DELETED
@@ -1,177 +0,0 @@
|
|
1
|
-
import {
|
2
|
-
modelText,
|
3
|
-
MigrationTuple,
|
4
|
-
MigrationError,
|
5
|
-
defaultMigrationCategory,
|
6
|
-
checkModelAlreadyExists,
|
7
|
-
setExecutedMigrations,
|
8
|
-
getExecutedMigrations,
|
9
|
-
lockMigrations,
|
10
|
-
MigrationCategories,
|
11
|
-
RunnableMigrations,
|
12
|
-
} from './utils';
|
13
|
-
import type { Tx } from '../database-layer/db';
|
14
|
-
import type { Config, Model } from '../config-loader/config-loader';
|
15
|
-
|
16
|
-
import * as _ from 'lodash';
|
17
|
-
import * as sbvrUtils from '../sbvr-api/sbvr-utils';
|
18
|
-
|
19
|
-
type ApiRootModel = Model & { apiRoot: string };
|
20
|
-
|
21
|
-
export const postRun = async (tx: Tx, model: ApiRootModel): Promise<void> => {
|
22
|
-
const { initSql } = model;
|
23
|
-
if (initSql == null) {
|
24
|
-
return;
|
25
|
-
}
|
26
|
-
|
27
|
-
const modelName = model.apiRoot;
|
28
|
-
|
29
|
-
const exists = await checkModelAlreadyExists(tx, modelName);
|
30
|
-
if (!exists) {
|
31
|
-
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
32
|
-
'First time executing, running init script',
|
33
|
-
);
|
34
|
-
|
35
|
-
await lockMigrations(tx, modelName, async () => {
|
36
|
-
try {
|
37
|
-
await tx.executeSql(initSql);
|
38
|
-
} catch (err) {
|
39
|
-
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
40
|
-
`postRun locked sql execution error ${err} `,
|
41
|
-
);
|
42
|
-
throw new MigrationError(err);
|
43
|
-
}
|
44
|
-
});
|
45
|
-
}
|
46
|
-
};
|
47
|
-
|
48
|
-
export const run = async (tx: Tx, model: ApiRootModel): Promise<void> => {
|
49
|
-
const { migrations } = model;
|
50
|
-
if (
|
51
|
-
migrations == null ||
|
52
|
-
_.isEmpty(migrations) ||
|
53
|
-
migrations[defaultMigrationCategory] === undefined
|
54
|
-
) {
|
55
|
-
return;
|
56
|
-
}
|
57
|
-
|
58
|
-
let runMigrations: RunnableMigrations = {};
|
59
|
-
for (const [key, value] of Object.entries(migrations)) {
|
60
|
-
if (key === defaultMigrationCategory && value instanceof Object) {
|
61
|
-
runMigrations = { ...runMigrations, ...value };
|
62
|
-
} else if (!(key in MigrationCategories)) {
|
63
|
-
runMigrations = { ...runMigrations, ...{ [key]: value } };
|
64
|
-
}
|
65
|
-
}
|
66
|
-
return $run(tx, model, runMigrations);
|
67
|
-
};
|
68
|
-
|
69
|
-
const $run = async (
|
70
|
-
tx: Tx,
|
71
|
-
model: ApiRootModel,
|
72
|
-
migrations: RunnableMigrations,
|
73
|
-
): Promise<void> => {
|
74
|
-
const modelName = model.apiRoot;
|
75
|
-
|
76
|
-
// migrations only run if the model has been executed before,
|
77
|
-
// to make changes that can't be automatically applied
|
78
|
-
const exists = await checkModelAlreadyExists(tx, modelName);
|
79
|
-
if (!exists) {
|
80
|
-
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
81
|
-
'First time model has executed, skipping migrations',
|
82
|
-
);
|
83
|
-
|
84
|
-
return await setExecutedMigrations(tx, modelName, Object.keys(migrations));
|
85
|
-
}
|
86
|
-
await lockMigrations(tx, modelName, async () => {
|
87
|
-
try {
|
88
|
-
const executedMigrations = await getExecutedMigrations(tx, modelName);
|
89
|
-
const pendingMigrations = filterAndSortPendingMigrations(
|
90
|
-
migrations,
|
91
|
-
executedMigrations,
|
92
|
-
);
|
93
|
-
if (pendingMigrations.length === 0) {
|
94
|
-
return;
|
95
|
-
}
|
96
|
-
|
97
|
-
const newlyExecutedMigrations = await executeMigrations(
|
98
|
-
tx,
|
99
|
-
pendingMigrations,
|
100
|
-
);
|
101
|
-
await setExecutedMigrations(tx, modelName, [
|
102
|
-
...executedMigrations,
|
103
|
-
...newlyExecutedMigrations,
|
104
|
-
]);
|
105
|
-
} catch (err) {
|
106
|
-
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
107
|
-
`$run executedMigrations error ${err}`,
|
108
|
-
);
|
109
|
-
throw new MigrationError(err);
|
110
|
-
}
|
111
|
-
});
|
112
|
-
};
|
113
|
-
|
114
|
-
// turns {"key1": migration, "key3": migration, "key2": migration}
|
115
|
-
// into [["key1", migration], ["key2", migration], ["key3", migration]]
|
116
|
-
const filterAndSortPendingMigrations = (
|
117
|
-
migrations: NonNullable<RunnableMigrations>,
|
118
|
-
executedMigrations: string[],
|
119
|
-
): MigrationTuple[] =>
|
120
|
-
(_(migrations).omit(executedMigrations) as _.Object<typeof migrations>)
|
121
|
-
.toPairs()
|
122
|
-
.sortBy(([migrationKey]) => migrationKey)
|
123
|
-
.value();
|
124
|
-
|
125
|
-
const executeMigrations = async (
|
126
|
-
tx: Tx,
|
127
|
-
migrations: MigrationTuple[] = [],
|
128
|
-
): Promise<string[]> => {
|
129
|
-
try {
|
130
|
-
for (const migration of migrations) {
|
131
|
-
await executeMigration(tx, migration);
|
132
|
-
}
|
133
|
-
} catch (err) {
|
134
|
-
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
135
|
-
'Error while executing migrations, rolled back',
|
136
|
-
);
|
137
|
-
throw new MigrationError(err);
|
138
|
-
}
|
139
|
-
return migrations.map(([migrationKey]) => migrationKey); // return migration keys
|
140
|
-
};
|
141
|
-
|
142
|
-
const executeMigration = async (
|
143
|
-
tx: Tx,
|
144
|
-
[key, migration]: MigrationTuple,
|
145
|
-
): Promise<void> => {
|
146
|
-
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
147
|
-
`Running migration ${JSON.stringify(key)}`,
|
148
|
-
);
|
149
|
-
|
150
|
-
if (typeof migration === 'function') {
|
151
|
-
await migration(tx, sbvrUtils);
|
152
|
-
} else if (typeof migration === 'string') {
|
153
|
-
await tx.executeSql(migration);
|
154
|
-
} else {
|
155
|
-
throw new MigrationError(`Invalid migration type: ${typeof migration}`);
|
156
|
-
}
|
157
|
-
};
|
158
|
-
|
159
|
-
export const config: Config = {
|
160
|
-
models: [
|
161
|
-
{
|
162
|
-
modelName: 'migrations',
|
163
|
-
apiRoot: 'migrations',
|
164
|
-
modelText,
|
165
|
-
migrations: {
|
166
|
-
'11.0.0-modified-at': `
|
167
|
-
ALTER TABLE "migration"
|
168
|
-
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
|
169
|
-
`,
|
170
|
-
'11.0.1-modified-at': `
|
171
|
-
ALTER TABLE "migration lock"
|
172
|
-
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
|
173
|
-
`,
|
174
|
-
},
|
175
|
-
},
|
176
|
-
],
|
177
|
-
};
|