Skip to content

Commit

Permalink
feat: singleton factory and theme service (#1298)
Browse files Browse the repository at this point in the history
  • Loading branch information
josephaxisa committed Apr 13, 2023
1 parent b51f782 commit 18af00b
Show file tree
Hide file tree
Showing 9 changed files with 604 additions and 2 deletions.
4 changes: 3 additions & 1 deletion packages/embed-services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
},
"homepage": "https://github.com/looker-open-source/sdk-codegen/tree/master/packages/embed-services",
"devDependencies": {
"@looker/sdk-node": "^23.4.0"
},
"dependencies": {
"@looker/sdk-rtl": "^21.6.0"
"@looker/sdk-rtl": "^21.6.0",
"@looker/sdk": "^23.6.0"
},
"keywords": [
"Looker",
Expand Down
56 changes: 56 additions & 0 deletions packages/embed-services/src/ServiceFactory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
MIT License
Copyright (c) 2023 Looker Data Sciences, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { Looker40SDK as LookerSDK } from '@looker/sdk'
import type { IAPIMethods } from '@looker/sdk-rtl'
import { session } from './test-utils'
import { createFactory, destroyFactory, getFactory } from './ServiceFactory'
import { getThemeService, registerThemeService } from './ThemeService'

describe('ServiceFactory', () => {
const sdk: IAPIMethods = new LookerSDK(session)

afterEach(() => {
destroyFactory()
})

it('createFactory creates', () => {
createFactory(sdk)
expect(getFactory()).toBeDefined()
})

it('getFactory throws when no factory exists', () => {
expect(getFactory).toThrow('Factory must be created with an SDK')
})

it('registers and gets a service', async () => {
createFactory(sdk)
registerThemeService()
const service = getThemeService()
expect(service).toBeDefined()
await service.getDefaultTheme()
expect(service.defaultTheme?.name).toEqual('Looker')
})
})
95 changes: 95 additions & 0 deletions packages/embed-services/src/ServiceFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
MIT License
Copyright (c) 2023 Looker Data Sciences, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import type { IAPIMethods } from '@looker/sdk-rtl'

export type ServiceCreatorFunc<T> = (sdk: IAPIMethods, timeToLive?: number) => T

export interface IServiceFactory {
get<T>(serviceName: string): T
register<T>(serviceName: string, serviceCreator: ServiceCreatorFunc<T>): void
}

/**
* A factory for registering and maintaining services
*/
class ServiceFactory implements IServiceFactory {
servicesMap: Record<string, any> = {}
constructor(private sdk: IAPIMethods) {}

get<T>(serviceName: string): T {
const service = this.servicesMap[serviceName]
if (!service) {
throw new Error(`Service ${serviceName} not found`)
}
return service
}

/**
* Registers or creates a service
* @param serviceName name of service.
* @param serviceCreator function that creates the service.
* @param timeToLive in seconds, for the service cache. Defaults to 15 minutes.
*/
register<T>(
serviceName: string,
serviceCreator: ServiceCreatorFunc<T>,
timeToLive?: number
) {
let service = this.servicesMap[serviceName]
if (!service) {
service = serviceCreator(this.sdk, timeToLive)
this.servicesMap[serviceName] = service
}
return service
}
}

let factory: IServiceFactory | undefined

/**
* Helper method for creating a singleton factory
* @param sdk
*/
export function createFactory(sdk: IAPIMethods) {
factory = new ServiceFactory(sdk)
}

/**
* Helper method for getting the factory
*/
export function getFactory() {
if (!factory) {
throw new Error('Factory must be created with an SDK.')
}
return factory
}

/**
* Helper method for destroying the factory
*/
export function destroyFactory() {
factory = undefined
}
183 changes: 183 additions & 0 deletions packages/embed-services/src/ThemeService.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
MIT License
Copyright (c) 2023 Looker Data Sciences, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import {
Looker40SDK as LookerSDK,
all_themes,
update_theme,
create_theme,
delete_theme,
search_themes,
} from '@looker/sdk'
import type { ITheme } from '@looker/sdk'
import type { IAPIMethods } from '@looker/sdk-rtl'

import { themeServiceCreator } from './ThemeService'
import type { IThemeService } from './ThemeService'
import { TestConfig, session, timeout } from './test-utils'

const config = TestConfig()
const themes = config.testData.themes

describe('ThemeService', () => {
const sdk: IAPIMethods = new LookerSDK(session)
let service: IThemeService
let testThemes: ITheme[]
const themeCount = themes.length + 1 // includes the Looker theme

beforeEach(() => {
service = themeServiceCreator(sdk)
})

const createTestThemes = async () => {
for (const t of themes) {
const searched = await sdk.ok(search_themes(sdk, { name: t.name }))
if (searched.length > 0) {
// update theme with expected values if found
await sdk.ok(update_theme(sdk, searched[0].id!, t))
} else {
// create theme if not found
await sdk.ok(create_theme(sdk, t))
}
}
}

const removeTestThemes = async () => {
for (const t of themes) {
const searched = await sdk.ok(search_themes(sdk, { id: t.id }))
if (searched.length > 0) {
await sdk.ok(delete_theme(sdk, searched[0].id!))
}
}
}

beforeAll(async () => {
await removeTestThemes()
await createTestThemes()
// get themes from instance to have their ids
testThemes = await sdk.ok(all_themes(sdk, 'id, name'))
}, timeout)

afterAll(async () => {
await sdk.authSession.logout()
})

describe('getAll', () => {
it('gets and caches', async () => {
await service.getAll()
expect(service.items).toHaveLength(themeCount)
expect(Object.keys(service.indexedItems)).toHaveLength(themeCount)
expect(service.expiresAt).toBeGreaterThan(0)
})
})

describe('get', () => {
it('gets and caches', async () => {
expect(service.items).toHaveLength(0)
const actual = await service.get(testThemes[0].id!)
expect(actual.name).toEqual(testThemes[0].name)
expect(service.indexedItems[testThemes[0].id!].name).toEqual(
testThemes[0].name
)
})

it('retrieves from cache when possible', async () => {
const themes = (await service.getAll()).items
const cachedTheme = themes[0]
const expectedName = cachedTheme.name + 'cached'
cachedTheme.name = expectedName
const actual = await service.get(cachedTheme.id!)
expect(actual.name).toEqual(expectedName)
})

it('bypasses cache when expired', async () => {
service = themeServiceCreator(sdk, -1000) // set time to live in the past
const themes = (await service.getAll()).items
const cachedTheme = themes[0]
const expectedName = cachedTheme.name
cachedTheme.name += 'cached'
const actual = await service.get(cachedTheme.id!)
expect(actual.name).toEqual(expectedName)
})

it('bypasses cache if cache=false', async () => {
service = themeServiceCreator(sdk)
const themes = (await service.getAll()).items
const cachedTheme = themes[0]
const expectedName = cachedTheme.name
cachedTheme.name += 'cached'
const actual = await service.get(cachedTheme.id!, { itemCache: false })
expect(actual.name).toEqual(expectedName)
})
})

describe('set', () => {
it('sets and caches', async () => {
const theme = testThemes.find((t) => t.name === themes[0].name)!
const updatedTheme = { ...theme, name: 'updated_theme' }
await service.set(updatedTheme.id!, updatedTheme)
expect(service.indexedItems[updatedTheme.id!].name).toEqual(
'updated_theme'
)
})
})

describe('delete', () => {
afterEach(async () => {
// recreate any deleted themes
await createTestThemes()
})

it('deletes', async () => {
const themes = (await service.getAll()).items
expect(themes).toHaveLength(themeCount)

const targetTheme = themes.find(
(t) => t.name !== 'Looker' // Default Looker theme cannot be deleted
)!
await service.delete(targetTheme.id!)

expect(service.items).toHaveLength(themeCount - 1)
expect(service.indexedItems[targetTheme.id!]).toBeUndefined()
})
})

describe('getDefaultTheme', () => {
it('gets default theme', async () => {
expect(service.defaultTheme).toBeUndefined()
await service.getDefaultTheme()
expect(service.defaultTheme).toBeDefined()
})
})

describe('load', () => {
it('loads', async () => {
expect(service.items).toHaveLength(0)
await service.load()
expect(service.items).toHaveLength(themeCount)
expect(service.defaultTheme?.name).toBe('Looker')
})
})
})
Loading

0 comments on commit 18af00b

Please sign in to comment.