import Service from '@ember/service' ; import { Format , CardEnv , CardOperation , JSONAPIDocument , Query , ResourceObject , assertDocumentDataIsResource , assertDocumentDataIsCollection , } from '@cardstack/core/src/interfaces' ; import CardModel from '@cardstack/core/src/card-model' ; import config from 'cardhost/config/environment' ; import { setComponentTemplate } from '@ember/component' ; import Component from '@glimmer/component' ; import { hbs } from 'ember-cli-htmlbars' ; import { tracked } from '@glimmer/tracking' ; import type Builder from 'cardhost/lib/builder' ; import { fetchJSON } from 'cardhost/lib/jsonapi-fetch' ; import { LOCAL_REALM } from 'cardhost/lib/builder' ; import { buildQueryString } from '@cardstack/core/src/query' ; const { cardServer } = config as any ; export default class Cards extends Service { private localRealmURL = LOCAL_REALM ; overrideRoutingCardWith : string | undefined ; async load ( url , format ) { let cardResponse ; if ( this . inLocalRealm ( url ) ) { let builder = await this . builder ( ) ; cardResponse = await builder . load ( url , format ) ; } else { cardResponse = await fetchJSON < JSONAPIDocument > ( this . buildCardURL ( { url , query : { format } } ) ) ; } let { data } = cardResponse ; assertDocumentDataIsResource ( data ) ; let { component , Model } = await this . codeForCard ( data ) ; return Model . fromResponse ( this . cardEnv ( ) , data , component ) ; } async loadForRoute ( pathname ) { if ( this . overrideRoutingCardWith ) { let builder = await this . builder ( ) ; return await builder . loadForRoute ( this . overrideRoutingCardWith , pathname ) ; } else { let url = `${ cardServer }cardFor${ pathname }` ; let cardResponse = await fetchJSON < JSONAPIDocument > ( url ) ; let { data } = cardResponse ; assertDocumentDataIsResource ( data ) ; let { component , Model } = await this . codeForCard ( data ) ; return Model . fromResponse ( this . cardEnv ( ) , data , component ) ; } } async query ( query ) { let { data } = await fetchJSON < JSONAPIDocument > ( this . buildCardURL ( { query } ) ) ; assertDocumentDataIsCollection ( data ) ; return await Promise . all ( data . map ( async ( cardResponse ) => { let { component , Model } = await this . codeForCard ( cardResponse ) ; return Model . fromResponse ( this . cardEnv ( ) , cardResponse , component ) ; } ) ) ; } private inLocalRealm ( cardURL ) { return cardURL . startsWith ( this . localRealmURL ) ; } private cardEnv ( ) { return { load : this . load . bind ( this ) , send : this . send . bind ( this ) , prepareComponent : this . prepareComponent . bind ( this ) , tracked : tracked as unknown as CardEnv [ 'tracked' ] , } ; } private _builderPromise : Promise < Builder > | undefined ; async builder ( ) { if ( this . _builderPromise ) { return this . _builderPromise ; } let resolve , reject ; this . _builderPromise = new Promise ( ( r , e ) => { resolve = r ; reject = e ; } ) ; try { let { default : Builder } = await import ( '../lib/builder' ) ; let builder = new Builder ( this . localRealmURL , this ) ; resolve ( builder ) ; return builder ; } catch ( err ) { reject ( err ) ; throw err ; } } private async send ( op ) { if ( this . operationIsLocal ( op ) ) { let builder = await this . builder ( ) ; return await builder . send ( op ) ; } if ( 'create' in op ) { return await fetchJSON < JSONAPIDocument > ( this . buildNewURL ( op . create . targetRealm , op . create . parentCardURL ) , { method : 'POST' , body : JSON . stringify ( op . create . payload ) , } ) ; } else if ( 'update' in op ) { return await fetchJSON < JSONAPIDocument > ( this . buildCardURL ( { url : op . update . cardURL } ) , { method : 'PATCH' , body : JSON . stringify ( op . update . payload ) , } ) ; } else { throw assertNever ( op ) ; } } private operationIsLocal ( op ) { if ( 'create' in op ) { return this . inLocalRealm ( op . create . targetRealm ) ; } else if ( 'update' in op ) { return this . inLocalRealm ( op . update . cardURL ) ; } else { throw assertNever ( op ) ; } } private buildNewURL ( realm , parentCardURL ) { return [ cardServer , 'cards/' , encodeURIComponent ( realm ) + '/' , encodeURIComponent ( parentCardURL ) , ] . join ( '' ) ; } private buildCardURL ( opts = { } ) { let fullURL = [ cardServer , 'cards/' ] ; if ( opts . url ) { fullURL . push ( encodeURIComponent ( opts . url ) ) ; } if ( opts . query ) { fullURL . push ( buildQueryString ( opts . query ) ) ; } return fullURL . join ( '' ) ; } private prepareComponent ( cardModel , component ) { return setComponentTemplate ( hbs `<this.component @model={{this.data}} @set={{this.set}} />` , class extends Component { component = component ; get data ( ) { return cardModel . data ; } set = cardModel . setters ; } ) ; } private async codeForCard ( card ) { let componentModule = card . meta ?. componentModule ; if ( ! componentModule ) { throw new Error ( 'No componentModule to load' ) ; } if ( typeof componentModule !== 'string' ) { throw new Error ( 'Cards component module is not a string' ) ; } let module = await this . loadModule < { default : unknown ; Model : typeof CardModel ; } > ( componentModule ) ; return { component : module . default , Model : module . Model , } ; } async loadModule < T extends object > ( moduleIdentifier ) { if ( moduleIdentifier . startsWith ( '@cardstack/compiled/' ) ) { moduleIdentifier = moduleIdentifier . replace ( '@cardstack/compiled/' , '' ) ; return await import ( `@cardstack/compiled/${ moduleIdentifier }` ) ; } else if ( moduleIdentifier . startsWith ( '@cardstack/core/src/' ) ) { moduleIdentifier = moduleIdentifier . replace ( '@cardstack/core/src/' , '' ) ; return await import ( `@cardstack/core/src/${ moduleIdentifier }` ) ; } else if ( moduleIdentifier . startsWith ( '@cardstack/boxel/' ) ) { moduleIdentifier = moduleIdentifier . replace ( '@cardstack/boxel/' , '' ) ; return await import ( `@cardstack/boxel/${ moduleIdentifier }` ) ; } else if ( moduleIdentifier . startsWith ( '@cardstack/local-realm-compiled/' ) ) { let builder = await this . builder ( ) ; return await builder . loadModule < T > ( moduleIdentifier ) ; } else { throw new Error ( `don't know how to load compiled card code for ${ moduleIdentifier }` ) ; } } } function assertNever ( value ) { throw new Error ( `unsupported operation ${ value }` ) ; }