sounding 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +334 -1
- package/bin/sounding.js +156 -0
- package/index.js +23 -0
- package/lib/create-app-manager.js +380 -26
- package/lib/create-auth-helpers.js +168 -21
- package/lib/create-browser-manager.js +578 -31
- package/lib/create-error.js +35 -0
- package/lib/create-expect.js +1070 -27
- package/lib/create-helper-runner.js +38 -2
- package/lib/create-mail-capture.js +125 -16
- package/lib/create-mailbox.js +20 -0
- package/lib/create-request-client.js +635 -57
- package/lib/create-runtime.js +222 -21
- package/lib/create-socket-manager.js +706 -0
- package/lib/create-test-api.js +491 -102
- package/lib/create-visit-client.js +40 -2
- package/lib/create-world-engine.js +106 -7
- package/lib/create-world-loader.js +150 -8
- package/lib/default-config.js +25 -0
- package/lib/define-world.js +27 -2
- package/lib/init-project.js +403 -0
- package/lib/merge-config.js +11 -0
- package/lib/normalize-config.js +16 -19
- package/lib/resolve-auth-config.js +36 -0
- package/lib/resolve-datastore.js +50 -7
- package/lib/resolve-dependency.js +145 -0
- package/lib/test-runner.js +427 -0
- package/lib/trial-context.js +29 -0
- package/lib/types.js +675 -0
- package/lib/validate-config.js +633 -0
- package/lib/validate-test-args.js +480 -0
- package/package.json +16 -2
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
|
|
4
|
+
const packageJson = require('../package.json')
|
|
5
|
+
|
|
6
|
+
const TEST_COMMAND = 'sounding test'
|
|
7
|
+
const SOUNDING_VERSION = `^${packageJson.version}`
|
|
8
|
+
const SQLITE_VERSION = packageJson.devDependencies?.['sails-sqlite'] || '^0.2.6'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{
|
|
12
|
+
* type: 'created' | 'updated' | 'skipped',
|
|
13
|
+
* path?: string,
|
|
14
|
+
* message: string,
|
|
15
|
+
* }} InitProjectAction
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* appPath?: string,
|
|
21
|
+
* config?: boolean,
|
|
22
|
+
* }} InitProjectOptions
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* appPath: string,
|
|
28
|
+
* auth: {
|
|
29
|
+
* identity: string,
|
|
30
|
+
* modelName: string,
|
|
31
|
+
* collection: string,
|
|
32
|
+
* scenario: string,
|
|
33
|
+
* detected: boolean,
|
|
34
|
+
* },
|
|
35
|
+
* actions: InitProjectAction[],
|
|
36
|
+
* }} InitProjectResult
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} value
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function titleCase(value) {
|
|
44
|
+
return `${value[0].toUpperCase()}${value.slice(1)}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} value
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function pluralize(value) {
|
|
52
|
+
if (value.endsWith('s')) {
|
|
53
|
+
return value
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return `${value}s`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} appPath
|
|
61
|
+
* @returns {InitProjectResult['auth']}
|
|
62
|
+
*/
|
|
63
|
+
function detectAuthConvention(appPath) {
|
|
64
|
+
const candidates = ['user', 'creator']
|
|
65
|
+
|
|
66
|
+
for (const identity of candidates) {
|
|
67
|
+
const modelName = titleCase(identity)
|
|
68
|
+
const modelPaths = [
|
|
69
|
+
path.join(appPath, 'api', 'models', `${modelName}.js`),
|
|
70
|
+
path.join(appPath, 'api', 'models', `${identity}.js`),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
if (modelPaths.some((modelPath) => fs.existsSync(modelPath))) {
|
|
74
|
+
return {
|
|
75
|
+
identity,
|
|
76
|
+
modelName,
|
|
77
|
+
collection: pluralize(identity),
|
|
78
|
+
scenario: `signed-in-${identity}`,
|
|
79
|
+
detected: true,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
identity: 'user',
|
|
86
|
+
modelName: 'User',
|
|
87
|
+
collection: 'users',
|
|
88
|
+
scenario: 'signed-in-user',
|
|
89
|
+
detected: false,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} filePath
|
|
95
|
+
* @returns {any}
|
|
96
|
+
*/
|
|
97
|
+
function readJsonFile(filePath) {
|
|
98
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} filePath
|
|
103
|
+
* @param {any} value
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
function writeJsonFile(filePath, value) {
|
|
107
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} appPath
|
|
112
|
+
* @param {string} targetPath
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function formatPath(appPath, targetPath) {
|
|
116
|
+
return path.relative(appPath, targetPath) || path.basename(targetPath)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {InitProjectAction[]} actions
|
|
121
|
+
* @param {string} appPath
|
|
122
|
+
* @param {string} directory
|
|
123
|
+
* @returns {void}
|
|
124
|
+
*/
|
|
125
|
+
function ensureDirectory(actions, appPath, directory) {
|
|
126
|
+
if (fs.existsSync(directory)) {
|
|
127
|
+
actions.push({
|
|
128
|
+
type: 'skipped',
|
|
129
|
+
path: directory,
|
|
130
|
+
message: `Kept existing ${formatPath(appPath, directory)}`,
|
|
131
|
+
})
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fs.mkdirSync(directory, { recursive: true })
|
|
136
|
+
actions.push({
|
|
137
|
+
type: 'created',
|
|
138
|
+
path: directory,
|
|
139
|
+
message: `Created ${formatPath(appPath, directory)}`,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {InitProjectAction[]} actions
|
|
145
|
+
* @param {string} appPath
|
|
146
|
+
* @param {string} filePath
|
|
147
|
+
* @param {string} contents
|
|
148
|
+
* @returns {void}
|
|
149
|
+
*/
|
|
150
|
+
function writeFileIfMissing(actions, appPath, filePath, contents) {
|
|
151
|
+
if (fs.existsSync(filePath)) {
|
|
152
|
+
actions.push({
|
|
153
|
+
type: 'skipped',
|
|
154
|
+
path: filePath,
|
|
155
|
+
message: `Kept existing ${formatPath(appPath, filePath)}`,
|
|
156
|
+
})
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
161
|
+
fs.writeFileSync(filePath, contents)
|
|
162
|
+
actions.push({
|
|
163
|
+
type: 'created',
|
|
164
|
+
path: filePath,
|
|
165
|
+
message: `Created ${formatPath(appPath, filePath)}`,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {any} pkg
|
|
171
|
+
* @param {string} name
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
174
|
+
function hasDependency(pkg, name) {
|
|
175
|
+
return Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name])
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {any} pkg
|
|
180
|
+
* @param {string} name
|
|
181
|
+
* @param {string} version
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
function ensureDevDependency(pkg, name, version) {
|
|
185
|
+
if (hasDependency(pkg, name)) {
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pkg.devDependencies ||= {}
|
|
190
|
+
pkg.devDependencies[name] = version
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {InitProjectAction[]} actions
|
|
196
|
+
* @param {string} appPath
|
|
197
|
+
* @returns {void}
|
|
198
|
+
*/
|
|
199
|
+
function updatePackageJson(actions, appPath) {
|
|
200
|
+
const packagePath = path.join(appPath, 'package.json')
|
|
201
|
+
const packageExisted = fs.existsSync(packagePath)
|
|
202
|
+
const pkg = packageExisted ? readJsonFile(packagePath) : {}
|
|
203
|
+
let changed = false
|
|
204
|
+
const details = []
|
|
205
|
+
|
|
206
|
+
pkg.scripts ||= {}
|
|
207
|
+
|
|
208
|
+
if (!pkg.scripts.test) {
|
|
209
|
+
pkg.scripts.test = TEST_COMMAND
|
|
210
|
+
changed = true
|
|
211
|
+
details.push('added `npm test`')
|
|
212
|
+
} else if (pkg.scripts.test !== TEST_COMMAND && !pkg.scripts['test:sounding']) {
|
|
213
|
+
pkg.scripts['test:sounding'] = TEST_COMMAND
|
|
214
|
+
changed = true
|
|
215
|
+
details.push('added `npm run test:sounding`')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (ensureDevDependency(pkg, 'sounding', SOUNDING_VERSION)) {
|
|
219
|
+
changed = true
|
|
220
|
+
details.push('added `sounding` devDependency')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (ensureDevDependency(pkg, 'sails-sqlite', SQLITE_VERSION)) {
|
|
224
|
+
changed = true
|
|
225
|
+
details.push('added `sails-sqlite` devDependency')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!changed) {
|
|
229
|
+
actions.push({
|
|
230
|
+
type: 'skipped',
|
|
231
|
+
path: packagePath,
|
|
232
|
+
message: 'Kept existing package.json Sounding setup',
|
|
233
|
+
})
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
writeJsonFile(packagePath, pkg)
|
|
238
|
+
actions.push({
|
|
239
|
+
type: packageExisted ? 'updated' : 'created',
|
|
240
|
+
path: packagePath,
|
|
241
|
+
message: `${packageExisted ? 'Updated' : 'Created'} package.json (${details.join(', ')})`,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {InitProjectResult['auth']} auth
|
|
247
|
+
* @returns {string}
|
|
248
|
+
*/
|
|
249
|
+
function buildFactoryTemplate(auth) {
|
|
250
|
+
return `module.exports = ({ defineFactory }) =>
|
|
251
|
+
defineFactory('${auth.identity}', ({ sequence }) => ({
|
|
252
|
+
email: sequence('${auth.identity}-email', (next) => \`${auth.identity}-\${next}@example.com\`),
|
|
253
|
+
fullName: 'Test ${auth.modelName}'
|
|
254
|
+
}))
|
|
255
|
+
`
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @param {InitProjectResult['auth']} auth
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
262
|
+
function buildScenarioTemplate(auth) {
|
|
263
|
+
return `module.exports = ({ defineScenario }) =>
|
|
264
|
+
defineScenario('${auth.scenario}', async ({ create }) => {
|
|
265
|
+
const member = await create('${auth.identity}')
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
${auth.collection}: {
|
|
269
|
+
member
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {InitProjectResult['auth']} auth
|
|
278
|
+
* @returns {string}
|
|
279
|
+
*/
|
|
280
|
+
function buildExamplesTemplate(auth) {
|
|
281
|
+
return `const { test } = require('sounding')
|
|
282
|
+
|
|
283
|
+
test('Sounding boots this Sails app', async ({ sails, expect }) => {
|
|
284
|
+
expect(sails.config.environment).toBe('test')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('virtual request example reaches Sails', async ({ get, expect }) => {
|
|
288
|
+
const response = await get('/')
|
|
289
|
+
|
|
290
|
+
expect(response.status).toBeDefined()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('helper trial example has access to Sails helpers', async ({ sails, expect }) => {
|
|
294
|
+
expect(sails.helpers).toBeDefined()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test.skip('authenticated request example', { world: '${auth.scenario}' }, async ({ request, world, expect }) => {
|
|
298
|
+
const response = await request.as('member').get('/account')
|
|
299
|
+
|
|
300
|
+
expect(response).toHaveStatus(200)
|
|
301
|
+
expect(response).toHaveSession('${auth.identity}Id', world.current.${auth.collection}.member.id)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test.skip('Inertia page contract example', async ({ visit, expect }) => {
|
|
305
|
+
const page = await visit('/dashboard')
|
|
306
|
+
|
|
307
|
+
expect(page).toBeInertiaPage('dashboard/index')
|
|
308
|
+
expect(page).toHaveNoInertiaErrors()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test.skip('captured mail example', async ({ sails, mailbox, expect }) => {
|
|
312
|
+
await sails.helpers.mail.send.with({
|
|
313
|
+
to: 'reader@example.com',
|
|
314
|
+
subject: 'Welcome',
|
|
315
|
+
template: 'welcome'
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
expect(mailbox).toHaveSentMail({ to: 'reader@example.com' })
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test.skip('browser journey example', { browser: true }, async ({ page, expect }) => {
|
|
322
|
+
await page.goto('/')
|
|
323
|
+
|
|
324
|
+
await expect(page).toBeDefined()
|
|
325
|
+
})
|
|
326
|
+
`
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @returns {string}
|
|
331
|
+
*/
|
|
332
|
+
function buildConfigTemplate() {
|
|
333
|
+
return `module.exports.sounding = {
|
|
334
|
+
// Defaults are enough for most apps. Keep app-specific overrides here.
|
|
335
|
+
environments: ['test']
|
|
336
|
+
}
|
|
337
|
+
`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @param {InitProjectOptions} [options]
|
|
342
|
+
* @returns {InitProjectResult}
|
|
343
|
+
*/
|
|
344
|
+
function initProject(options = {}) {
|
|
345
|
+
const appPath = path.resolve(options.appPath || process.cwd())
|
|
346
|
+
/** @type {InitProjectAction[]} */
|
|
347
|
+
const actions = []
|
|
348
|
+
const auth = detectAuthConvention(appPath)
|
|
349
|
+
|
|
350
|
+
updatePackageJson(actions, appPath)
|
|
351
|
+
|
|
352
|
+
const testsPath = path.join(appPath, 'tests')
|
|
353
|
+
const factoriesPath = path.join(testsPath, 'factories')
|
|
354
|
+
const scenariosPath = path.join(testsPath, 'scenarios')
|
|
355
|
+
const examplesPath = path.join(testsPath, 'sounding')
|
|
356
|
+
|
|
357
|
+
ensureDirectory(actions, appPath, testsPath)
|
|
358
|
+
ensureDirectory(actions, appPath, factoriesPath)
|
|
359
|
+
ensureDirectory(actions, appPath, scenariosPath)
|
|
360
|
+
ensureDirectory(actions, appPath, examplesPath)
|
|
361
|
+
|
|
362
|
+
writeFileIfMissing(
|
|
363
|
+
actions,
|
|
364
|
+
appPath,
|
|
365
|
+
path.join(factoriesPath, `${auth.identity}.js`),
|
|
366
|
+
buildFactoryTemplate(auth)
|
|
367
|
+
)
|
|
368
|
+
writeFileIfMissing(
|
|
369
|
+
actions,
|
|
370
|
+
appPath,
|
|
371
|
+
path.join(scenariosPath, `${auth.scenario}.js`),
|
|
372
|
+
buildScenarioTemplate(auth)
|
|
373
|
+
)
|
|
374
|
+
writeFileIfMissing(
|
|
375
|
+
actions,
|
|
376
|
+
appPath,
|
|
377
|
+
path.join(examplesPath, 'examples.test.js'),
|
|
378
|
+
buildExamplesTemplate(auth)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
const configPath = path.join(appPath, 'config', 'sounding.js')
|
|
382
|
+
if (options.config) {
|
|
383
|
+
writeFileIfMissing(actions, appPath, configPath, buildConfigTemplate())
|
|
384
|
+
} else {
|
|
385
|
+
actions.push({
|
|
386
|
+
type: 'skipped',
|
|
387
|
+
path: configPath,
|
|
388
|
+
message: 'Skipped config/sounding.js because Sounding defaults are enough',
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
appPath,
|
|
394
|
+
auth,
|
|
395
|
+
actions,
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = {
|
|
400
|
+
TEST_COMMAND,
|
|
401
|
+
detectAuthConvention,
|
|
402
|
+
initProject,
|
|
403
|
+
}
|
package/lib/merge-config.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {any} value
|
|
5
|
+
* @returns {value is AnyRecord}
|
|
6
|
+
*/
|
|
1
7
|
function isPlainObject(value) {
|
|
2
8
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
3
9
|
}
|
|
4
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @param {AnyRecord} base
|
|
13
|
+
* @param {AnyRecord} override
|
|
14
|
+
* @returns {AnyRecord}
|
|
15
|
+
*/
|
|
5
16
|
function mergeConfig(base, override) {
|
|
6
17
|
const output = { ...base }
|
|
7
18
|
|
package/lib/normalize-config.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
2
|
+
/** @typedef {import('./types').SoundingUserConfig} SoundingUserConfig */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {any} value
|
|
6
|
+
* @returns {value is AnyRecord}
|
|
7
|
+
*/
|
|
1
8
|
function isPlainObject(value) {
|
|
2
9
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
3
10
|
}
|
|
4
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @param {any} datastore
|
|
14
|
+
* @returns {any}
|
|
15
|
+
*/
|
|
5
16
|
function normalizeDatastore(datastore) {
|
|
6
17
|
if (typeof datastore === 'string') {
|
|
7
18
|
return {
|
|
@@ -13,27 +24,13 @@ function normalizeDatastore(datastore) {
|
|
|
13
24
|
return datastore
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
const legacyManaged = isPlainObject(normalized.managed) ? normalized.managed : {}
|
|
18
|
-
|
|
19
|
-
if (normalized.adapter == null && legacyManaged.adapter != null) {
|
|
20
|
-
normalized.adapter = legacyManaged.adapter
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (normalized.root == null) {
|
|
24
|
-
normalized.root = legacyManaged.root ?? legacyManaged.directory
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (normalized.isolation == null && legacyManaged.isolation != null) {
|
|
28
|
-
normalized.isolation = legacyManaged.isolation
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
delete normalized.managed
|
|
32
|
-
delete normalized.directory
|
|
33
|
-
|
|
34
|
-
return normalized
|
|
27
|
+
return { ...datastore }
|
|
35
28
|
}
|
|
36
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @param {any} [config]
|
|
32
|
+
* @returns {SoundingUserConfig}
|
|
33
|
+
*/
|
|
37
34
|
function normalizeUserConfig(config = {}) {
|
|
38
35
|
if (!isPlainObject(config)) {
|
|
39
36
|
return {}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
2
|
+
/** @typedef {import('./types').SoundingSailsApp} SoundingSailsApp */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {any} value
|
|
6
|
+
* @returns {value is AnyRecord}
|
|
7
|
+
*/
|
|
1
8
|
function isPlainObject(value) {
|
|
2
9
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
3
10
|
}
|
|
4
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @param {any} value
|
|
14
|
+
* @returns {string | null}
|
|
15
|
+
*/
|
|
5
16
|
function normalizeIdentity(value) {
|
|
6
17
|
if (typeof value !== 'string') {
|
|
7
18
|
return null
|
|
@@ -11,6 +22,10 @@ function normalizeIdentity(value) {
|
|
|
11
22
|
return normalized || null
|
|
12
23
|
}
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @param {string | null} value
|
|
27
|
+
* @returns {string | null}
|
|
28
|
+
*/
|
|
14
29
|
function titleCase(value) {
|
|
15
30
|
if (!value) {
|
|
16
31
|
return value
|
|
@@ -19,10 +34,19 @@ function titleCase(value) {
|
|
|
19
34
|
return `${value[0].toUpperCase()}${value.slice(1)}`
|
|
20
35
|
}
|
|
21
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @param {{ sails?: SoundingSailsApp, getConfig?: () => AnyRecord }} [input]
|
|
39
|
+
* @returns {AnyRecord}
|
|
40
|
+
*/
|
|
22
41
|
function resolveSoundingConfig({ sails, getConfig } = {}) {
|
|
23
42
|
return (typeof getConfig === 'function' ? getConfig() : null) || sails?.config?.sounding || {}
|
|
24
43
|
}
|
|
25
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @param {SoundingSailsApp} sails
|
|
47
|
+
* @param {string | null} identity
|
|
48
|
+
* @returns {AnyRecord | null}
|
|
49
|
+
*/
|
|
26
50
|
function getModelByIdentity(sails, identity) {
|
|
27
51
|
const normalizedIdentity = normalizeIdentity(identity)
|
|
28
52
|
|
|
@@ -33,6 +57,10 @@ function getModelByIdentity(sails, identity) {
|
|
|
33
57
|
return sails?.models?.[normalizedIdentity] || sails?.models?.[titleCase(normalizedIdentity)] || null
|
|
34
58
|
}
|
|
35
59
|
|
|
60
|
+
/**
|
|
61
|
+
* @param {SoundingSailsApp} sails
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
36
64
|
function detectModelIdentity(sails) {
|
|
37
65
|
for (const candidate of ['user', 'creator']) {
|
|
38
66
|
if (getModelByIdentity(sails, candidate)) {
|
|
@@ -43,6 +71,10 @@ function detectModelIdentity(sails) {
|
|
|
43
71
|
return 'user'
|
|
44
72
|
}
|
|
45
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @param {AnyRecord} [authConfig]
|
|
76
|
+
* @returns {AnyRecord}
|
|
77
|
+
*/
|
|
46
78
|
function resolvePasswordConfig(authConfig = {}) {
|
|
47
79
|
const passwordConfig = isPlainObject(authConfig.password) ? authConfig.password : {}
|
|
48
80
|
const formConfig = isPlainObject(passwordConfig.form) ? passwordConfig.form : {}
|
|
@@ -71,6 +103,10 @@ function resolvePasswordConfig(authConfig = {}) {
|
|
|
71
103
|
}
|
|
72
104
|
}
|
|
73
105
|
|
|
106
|
+
/**
|
|
107
|
+
* @param {{ sails?: SoundingSailsApp, getConfig?: () => AnyRecord }} [input]
|
|
108
|
+
* @returns {AnyRecord}
|
|
109
|
+
*/
|
|
74
110
|
function resolveAuthConfig({ sails, getConfig } = {}) {
|
|
75
111
|
const soundingConfig = resolveSoundingConfig({ sails, getConfig })
|
|
76
112
|
const authConfig = isPlainObject(soundingConfig.auth) ? soundingConfig.auth : {}
|
package/lib/resolve-datastore.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
const path = require('node:path')
|
|
2
|
+
const { createSoundingError } = require('./create-error')
|
|
2
3
|
|
|
4
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
5
|
+
/** @typedef {import('./types').SoundingConfig} SoundingConfig */
|
|
6
|
+
/** @typedef {import('./types').SoundingDatastoreState} SoundingDatastoreState */
|
|
7
|
+
/** @typedef {import('./types').SoundingSailsApp} SoundingSailsApp */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {NodeJS.ProcessEnv | AnyRecord} [env]
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
3
13
|
function getWorkerToken(env = process.env) {
|
|
4
14
|
return (
|
|
5
15
|
env.SOUNDING_WORKER_INDEX ||
|
|
@@ -9,6 +19,15 @@ function getWorkerToken(env = process.env) {
|
|
|
9
19
|
)
|
|
10
20
|
}
|
|
11
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @param {{
|
|
24
|
+
* root?: string,
|
|
25
|
+
* identity?: string,
|
|
26
|
+
* isolation?: string,
|
|
27
|
+
* env?: NodeJS.ProcessEnv | AnyRecord,
|
|
28
|
+
* }} options
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
12
31
|
function buildManagedSqlitePath({
|
|
13
32
|
root = path.join(process.cwd(), '.tmp', 'db'),
|
|
14
33
|
identity = 'default',
|
|
@@ -19,6 +38,10 @@ function buildManagedSqlitePath({
|
|
|
19
38
|
return path.join(root, identity, `${workerToken}.db`)
|
|
20
39
|
}
|
|
21
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @param {{ sails?: SoundingSailsApp, datastore?: AnyRecord, appPath?: string }} options
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
22
45
|
function resolveManagedRoot({ sails, datastore = {}, appPath } = {}) {
|
|
23
46
|
const configuredRoot = datastore.root || path.join('.tmp', 'db')
|
|
24
47
|
|
|
@@ -29,7 +52,12 @@ function resolveManagedRoot({ sails, datastore = {}, appPath } = {}) {
|
|
|
29
52
|
return path.resolve(appPath || sails?.config?.appPath || process.cwd(), configuredRoot)
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
/**
|
|
56
|
+
* @param {{ sails: SoundingSailsApp, soundingConfig: SoundingConfig }} input
|
|
57
|
+
* @returns {SoundingDatastoreState & { managed: boolean, filePath?: string }}
|
|
58
|
+
*/
|
|
32
59
|
function resolveDatastore({ sails, soundingConfig }) {
|
|
60
|
+
/** @type {AnyRecord} */
|
|
33
61
|
const datastoreConfig = soundingConfig.datastore || {}
|
|
34
62
|
const mode = datastoreConfig.mode || 'managed'
|
|
35
63
|
const identity = datastoreConfig.identity || 'default'
|
|
@@ -39,9 +67,14 @@ function resolveDatastore({ sails, soundingConfig }) {
|
|
|
39
67
|
const configuredDatastore = datastores[identity]
|
|
40
68
|
|
|
41
69
|
if (!configuredDatastore) {
|
|
42
|
-
throw
|
|
43
|
-
|
|
44
|
-
|
|
70
|
+
throw createSoundingError({
|
|
71
|
+
code: 'E_SOUNDING_DATASTORE_CONFIG_MISSING',
|
|
72
|
+
message: `Sounding could not find datastore \`${identity}\` in sails.config.datastores for mode \`${mode}\`.`,
|
|
73
|
+
details: {
|
|
74
|
+
mode,
|
|
75
|
+
identity,
|
|
76
|
+
},
|
|
77
|
+
})
|
|
45
78
|
}
|
|
46
79
|
|
|
47
80
|
return {
|
|
@@ -56,9 +89,13 @@ function resolveDatastore({ sails, soundingConfig }) {
|
|
|
56
89
|
const adapter = datastoreConfig.adapter || 'sails-sqlite'
|
|
57
90
|
|
|
58
91
|
if (adapter !== 'sails-sqlite') {
|
|
59
|
-
throw
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
throw createSoundingError({
|
|
93
|
+
code: 'E_SOUNDING_DATASTORE_ADAPTER_UNSUPPORTED',
|
|
94
|
+
message: `Sounding only supports managed adapter \`sails-sqlite\` in v0.0.1. Received \`${adapter}\`.`,
|
|
95
|
+
details: {
|
|
96
|
+
adapter,
|
|
97
|
+
},
|
|
98
|
+
})
|
|
62
99
|
}
|
|
63
100
|
|
|
64
101
|
const filePath = buildManagedSqlitePath({
|
|
@@ -87,7 +124,13 @@ function resolveDatastore({ sails, soundingConfig }) {
|
|
|
87
124
|
}
|
|
88
125
|
}
|
|
89
126
|
|
|
90
|
-
throw
|
|
127
|
+
throw createSoundingError({
|
|
128
|
+
code: 'E_SOUNDING_DATASTORE_MODE_UNKNOWN',
|
|
129
|
+
message: `Unknown Sounding datastore mode: ${mode}`,
|
|
130
|
+
details: {
|
|
131
|
+
mode,
|
|
132
|
+
},
|
|
133
|
+
})
|
|
91
134
|
}
|
|
92
135
|
|
|
93
136
|
module.exports = {
|