UNPKG

@sprucelabs/spruce-cli

Version:

Command line interface for building Spruce skills.

564 lines (489 loc) • 17.6 kB
import { Schema, SchemaTemplateItem } from '@sprucelabs/schema' import { diskUtil, namesUtil, versionUtil, } from '@sprucelabs/spruce-skill-utils' import { test, assert } from '@sprucelabs/test-utils' import { errorAssert } from '@sprucelabs/test-utils' import { FeatureActionResponse } from '../../../features/features.types' import SchemaTemplateItemBuilder from '../../../templateItemBuilders/SchemaTemplateItemBuilder' import AbstractSkillTest from '../../../tests/AbstractSkillTest' import testUtil from '../../../tests/utilities/test.utility' const LOCAL_NAMESPACE = 'TestingSchemas' const REMOTE_NAMESPACE = 'RemoteSchemas' const REMOTE_MODULE = '@sprucelabs/calendar-utils' const REMOTE_MODULE2 = '@sprucelabs/spruce-templates' export default class SettingUpSchemasForModuleDistributionTest extends AbstractSkillTest { protected static skillCacheKey = 'schemas' private static builder1: Schema private static schema1: Schema = { id: 'calendarEvent', version: 'v2021_09_20', namespace: 'TestingSchemas', name: 'Schema imported as module', moduleToImportFromWhenRemote: '@sprucelabs/calendar-utils', fields: { fieldName1: { label: 'First Field', type: 'text', isRequired: true, }, fieldName2: { label: 'Second Field', type: 'number', isRequired: true, hint: 'A hint', }, }, } //@ts-ignore private static schema1b: Schema private static schema2: Schema = { id: 'calendarEvent2', version: 'v2021_09_20', namespace: 'TestingSchemas', name: 'Schema imported as module', moduleToImportFromWhenRemote: '@sprucelabs/spruce-templates', fields: { fieldName1: { label: 'First Field', type: 'text', isRequired: true, }, fieldName2: { label: 'Second Field', type: 'number', isRequired: true, hint: 'A hint', }, }, } @test( 'can specify importFromModuleWhenRemote 1', 'CalendarEvent', REMOTE_MODULE, '1' ) @test( 'can specify importFromModuleWhenRemote 2', 'CalendarEvent2', REMOTE_MODULE2, '2' ) protected static async canSpecifyModuleToImportFromWhenRemote( namePascal: string, remoteModule: string, count: '1' | '2' ) { const nameCamel = namesUtil.toCamel(namePascal) const promise = this.Action('schema', 'create').execute({ nameReadable: 'Schema imported as module', namePascal, nameCamel, moduleToImportFromWhenRemote: remoteModule, }) await this.wait(10) if (this.ui.isWaitingForInput()) { await this.ui.sendInput(versionUtil.generateVersion().constValue) } const results = await promise assert.isFalsy(results.errors) const builderFile = testUtil.assertFileByNameInGeneratedFiles( `${nameCamel}.builder.ts`, results.files ) const builder = await this.Service('import').importDefault(builderFile) assert.isEqual(builder.moduleToImportFromWhenRemote, remoteModule) //@ts-ignore this[`builder${count}`] = builder const schemaFile = testUtil.assertFileByNameInGeneratedFiles( `${nameCamel}.schema.ts`, results.files ) const schema = await this.Service('import').importDefault(schemaFile) assert.isEqual(schema.moduleToImportFromWhenRemote, remoteModule) //@ts-ignore this[`schema${count}`] = schema //@ts-ignore this[`schema${count}b`] = { ...schema, id: `schema${count}B` } } @test() protected static async templateItemGeneratorHonorsModuleToImportWhenRemoteWhenInRemoteNamespace() { const templateItems = this.generateTemplateItems(REMOTE_NAMESPACE) for (const templateItem of templateItems) { assert.isEqual( templateItem.schema.moduleToImportFromWhenRemote, REMOTE_MODULE ) assert.isEqual(templateItem.importFrom, REMOTE_MODULE) } } @test() protected static async moduleToImportWhenRemoteIgnoredWhenInSameNamespace() { const templateItems = this.generateTemplateItems(LOCAL_NAMESPACE) for (const templateItem of templateItems) { assert.isEqual( templateItem.schema.moduleToImportFromWhenRemote, REMOTE_MODULE ) assert.isFalse('importFrom' in templateItem) } } @test( 'should ask about missing module and cancel install', ['schema1'], [REMOTE_MODULE], false ) @test( 'should ask about missing module and do install', ['schema1'], [REMOTE_MODULE], true ) @test( 'should ask about missing module and do install', ['schema1', 'schema2'], [REMOTE_MODULE, REMOTE_MODULE2], true ) @test( 'does not return the same module twice', ['schema1', 'schema1b', 'schema2'], [REMOTE_MODULE, REMOTE_MODULE2], true ) protected static async asksIfModuleShouldBeInstalled( schemaNames: ('schema1' | 'schema2')[], expectedModules: string[], shouldConfirmInstall: boolean ) { await this.reInstallSkill() await this.Store('skill').setCurrentSkillsNamespace('TestingTwo') const schemas = schemaNames.map((name) => this[name]) await this.emitter.on('schema.did-fetch-schemas', async () => { return { schemas, } }) const promise = this.SyncAction().execute({}) await this.waitForInput() const secondToLast = this.ui.invocations[this.ui.invocations.length - 2] assert.isEqual( secondToLast.options.headline, `Missing ${expectedModules.length} module${ expectedModules.length === 1 ? '' : 's' }` ) for (const expected of expectedModules) { assert.doesInclude(secondToLast.options.lines, expected) } await this.ui.sendInput(shouldConfirmInstall ? 'y' : 'n') const results = await promise if (shouldConfirmInstall) { assert.isFalsy(results.errors) const pkg = this.Service('pkg') for (const expected of expectedModules) { assert.isTrue(pkg.isInstalled(expected)) } } else { assert.isTruthy(results.errors) errorAssert.assertError(results.errors[0], 'ACTION_CANCELLED') } } @test('schemas are imported from one module', ['schema1']) @test('schemas are imported from two modules', ['schema1', 'schema2']) protected static async schemasAreImportedFromModuleWhenSyncedFromDifferentNamespace( schemaNames: ('schema1' | 'schema2')[] ) { await this.reInstallSkill() await this.Store('skill').setCurrentSkillsNamespace('TestingTwo') const schemas: Schema[] = [] for (const name of schemaNames) { schemas.push(this[name]) } await this.emitter.on('schema.did-fetch-schemas', async () => { return { schemas, } }) const results = await this.SyncAction().execute({ shouldInstallMissingDependencies: true, }) await this.assertCalendarEventSchemaIsImported(results) assert.doesThrow(() => testUtil.assertFileByNameInGeneratedFiles( 'schemas.types.ts', results.files ) ) } @test('can reference impored from another module', [ `import * as HeartwoodTypesLocal from '@sprucelabs/heartwood-view-controllers'`, ]) @test( 'can reference impored from another module with two importsWhenLocal', [ `import * as HeartwoodTypesLocal from '@sprucelabs/heartwood-view-controllers'`, `import * as HeartwoodTypesLocal2 from '@sprucelabs/heartwood-view-controllers'`, ] ) protected static async canReferenceSchemaImportedFromAnotherModule( importsWhenRemote: string[] ) { await this.reInstallSkill() await this.Store('skill').setCurrentSkillsNamespace('TestingTwo') const mySchema = { id: 'localSchema', importsWhenRemote, fields: { calendarEvent: { type: 'schema', options: { schema: { ...this.schema1, namespace: 'CalendarUtils', version: 'v2021_05_19', }, }, }, }, } await this.emitter.on('schema.did-fetch-schemas', async () => { return { schemas: [mySchema], } }) const results = await this.SyncAction().execute({ shouldInstallMissingDependencies: true, }) const localSchemaFile = testUtil.assertFileByNameInGeneratedFiles( 'localSchema.schema.ts', results.files ) const localSchema = await this.Service('import').importDefault(localSchemaFile) assert.isEqualDeep(localSchema.importsWhenRemote, importsWhenRemote) await this.assertCalendarEventSchemaIsImported(results) await this.assertValidTypesFileWhenAddingRemoteImport(results) } @test() protected static async canSetImportFromModuleWithNestedSchemasWhenRemoteWhenSyncing() { await this.reInstallSkill() await this.Action('schema', 'create').execute({ nameReadable: 'Schema imported as module', namePascal: 'CalandarEvent', nameCamel: 'calendarEvent', }) const results = await this.SyncAction().execute({ moduleToImportFromWhenRemote: REMOTE_MODULE, }) this.schema1 = (await this.importGeneratedFile( results, `calendarEvent.schema.ts` )) as Schema assert.isEqual(this.schema1.moduleToImportFromWhenRemote, REMOTE_MODULE) } @test() protected static async canHandleFormBuilderImportExportObject() { const version = versionUtil.generateVersion().constValue const completedFormBuilder = { id: 'fullCompletedForm', version, moduleToImportFromWhenRemote: '@sprucelabs/heartwood-view-controllers', fields: { id: { type: 'id', isRequired: true, }, personId: { type: 'id', isRequired: true, }, values: { type: 'raw', isArray: true, options: { valueType: 'Record<string, any>', }, }, }, } const builderForm = { id: 'builtForm', version, moduleToImportFromWhenRemote: '@sprucelabs/heartwood-view-controllers', fields: { id: { type: 'id', isRequired: true, }, dateDeleted: { type: 'number', }, completedFormBuilder: { type: 'schema', options: { schemaId: { version, id: 'fullCompletedForm', }, }, }, }, } const itemBuilder = new SchemaTemplateItemBuilder('TestSkill') const results = itemBuilder.buildTemplateItems({ //@ts-ignore TestSkill: [completedFormBuilder, builderForm], }) assert.isFalsy( ( results.find((r) => r.id === 'builtForm')?.schema.fields ?.completedFormBuilder?.options as any )?.schemaIds?.[0]?.moduleToImportFromWhenRemote ) } @test() protected static async importedSchemasDoNotShowUpInSchemaTypes() { // await this.reInstallSkill() await this.Store('skill').setCurrentSkillsNamespace('TestingTwo') const schema1 = { id: 'fullCompletedForm', namespace: 'Heartwood', version: versionUtil.generateVersion().dirValue, moduleToImportFromWhenRemote: '@sprucelabs/heartwood-view-controllers', importsWhenRemote: ['import * from bananarama'], fields: { id: { type: 'id', isRequired: true, }, personId: { type: 'id', isRequired: true, }, values: { type: 'raw', isArray: true, options: { valueType: 'Record<string, any>', }, }, }, } const schema2 = { id: 'localCompleted', version: versionUtil.generateVersion().dirValue, fields: { id: { type: 'id', isRequired: true, }, personId: { type: 'id', isRequired: true, }, values: { type: 'raw', isArray: true, options: { valueType: 'Record<string, any>', }, }, }, } await this.emitter.on('schema.did-fetch-schemas', async () => { return { schemas: [schema1, schema2], } }) const results = await this.SyncAction().execute({ shouldInstallMissingDependencies: true, }) assert.isFalsy(results.errors) const schemaTypesFile = testUtil.assertFileByNameInGeneratedFiles( 'schemas.types.ts', results.files ) await this.Service('typeChecker').check(schemaTypesFile) } private static SyncAction() { return this.Action('schema', 'sync', { optionOverrides: { 'sync.schemas': {}, }, }) } private static async importGeneratedFile( results: FeatureActionResponse, fileName: string ) { const schemaFile = testUtil.assertFileByNameInGeneratedFiles( fileName, results.files ) return await this.Service('import').importDefault(schemaFile) } private static async assertCalendarEventSchemaIsImported( results: FeatureActionResponse ) { assert.isFalsy(results.errors) const imported = testUtil.assertFileByNameInGeneratedFiles( 'calendarEvent.schema.ts', results.files ) const calendarEventContents = diskUtil.readFile(imported) assert.isEqual( calendarEventContents.trim(), `export { calendarEventSchema as default } from '@sprucelabs/calendar-utils'` ) } private static generateTemplateItems(namespace: string) { const itemBuilder = new SchemaTemplateItemBuilder(namespace) const templateItems = itemBuilder.buildTemplateItems( { [LOCAL_NAMESPACE]: [ { ...this.builder1, fields: { ...this.builder1.fields, nestedSchema: { type: 'schema', options: { schema: { id: 'nested', fields: { firstName: { type: 'text', }, }, }, }, }, }, }, ], }, this.resolveHashSprucePath('schemas') ) return templateItems as SchemaTemplateItem[] } private static async assertValidTypesFileWhenAddingRemoteImport( results: FeatureActionResponse ) { const schemaTypesFile = testUtil.assertFileByNameInGeneratedFiles( 'schemas.types.ts', results.files ) const typesContent = `import '${REMOTE_MODULE}'\n\n${diskUtil.readFile( schemaTypesFile )}` diskUtil.writeFile(schemaTypesFile, typesContent) await this.Service('typeChecker').check(schemaTypesFile) } }