Skip to content

Commit fbfdf43

Browse files
feat(dev-portal-api): INFRA-1582 new fields to B2CApp for webTransform and auto-publish (#7439)
* feat(dev-portal-api): INFRA-1582 add web-transform support * chore(dev-portal-web): INFRA-1582 sync module * feat(dev-portal-api): INFRA-1582 add trackable publishing fields to B2CApp * chore(dev-portal-api): INFRA-1582 small test adjustment * chore(dev-portal-api): INFRA-1582 add extra tests to type field
1 parent c3e957c commit fbfdf43

File tree

15 files changed

+797
-64
lines changed

15 files changed

+797
-64
lines changed

apps/dev-portal-api/domains/condo/gql.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { generateGqlQueries } = require('@open-condo/codegen/generate.gql')
33
const CondoB2BAppGql = generateGqlQueries('B2BApp', '{ id oidcClient { id deletedAt } }')
44
const CondoB2BAppContextGql = generateGqlQueries('B2BAppContext', '{ id status deletedAt organization { id name tin deletedAt } app { importId importRemoteSystem } }')
55

6-
const CondoB2CAppGql = generateGqlQueries('B2CApp', '{ id oidcClient { id deletedAt } }')
6+
const CondoB2CAppGql = generateGqlQueries('B2CApp', '{ id appUrl oidcClient { id deletedAt } }')
77
const CondoB2CAppBuildGql = generateGqlQueries('B2CAppBuild', '{ id }')
88
const CondoB2CAppPropertyGql = generateGqlQueries('B2CAppProperty', '{ id address deletedAt app { importId importRemoteSystem } }')
99
const CondoOIDCClientGql = generateGqlQueries('OidcClient', '{ id clientId payload name isEnabled }')

apps/dev-portal-api/domains/miniapp/constants/b2c.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ const path = require('path')
33
const DEFAULT_COLOR_SCHEMA = { 'main':'#707695', 'secondary':'#F2F4F6' }
44
const DEFAULT_LOGO_PATH = path.resolve(__dirname, '..', 'assets', 'b2c.png')
55

6+
const B2C_APP_CORDOVA_TYPE = 'cordova'
7+
const B2C_APP_WEB_TYPE = 'web'
8+
const B2C_APP_TYPES = [B2C_APP_CORDOVA_TYPE, B2C_APP_WEB_TYPE]
9+
610
module.exports = {
711
DEFAULT_COLOR_SCHEMA,
812
DEFAULT_LOGO_PATH,
13+
14+
B2C_APP_TYPES,
15+
B2C_APP_CORDOVA_TYPE,
16+
B2C_APP_WEB_TYPE,
917
}

apps/dev-portal-api/domains/miniapp/gql.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ const { gql } = require('graphql-tag')
77

88
const { generateGqlQueries } = require('@open-condo/codegen/generate.gql')
99

10-
const { AVAILABLE_ENVIRONMENTS } = require('@dev-portal-api/domains/miniapp/constants/publishing')
10+
const { getEnvironmentalFieldsSelection } = require('./schema/fields/environmental')
1111

1212
const COMMON_FIELDS = 'id dv sender { dv fingerprint } v deletedAt newId createdBy { id name } updatedBy { id name } createdAt updatedAt'
13-
const EXPORT_FIELDS = AVAILABLE_ENVIRONMENTS.map(environment => `${environment}ExportId`).join(' ')
13+
const EXPORT_FIELDS = getEnvironmentalFieldsSelection(['exportId'])
1414

1515
const B2B_APP_FIELDS = `{ name developer logo { publicUrl originalFilename } ${COMMON_FIELDS} ${EXPORT_FIELDS} }`
1616
const B2BApp = generateGqlQueries('B2BApp', B2B_APP_FIELDS)
@@ -27,7 +27,7 @@ const PUBLISH_B2B_APP_MUTATION = gql`
2727

2828

2929

30-
const B2C_APP_FIELDS = `{ name developer logo { publicUrl originalFilename } ${COMMON_FIELDS} ${EXPORT_FIELDS} }`
30+
const B2C_APP_FIELDS = `{ name type developer logo { publicUrl originalFilename } ${getEnvironmentalFieldsSelection(['webTransformEnabled', 'publishedAt'])} ${COMMON_FIELDS} ${EXPORT_FIELDS} }`
3131
const B2CApp = generateGqlQueries('B2CApp', B2C_APP_FIELDS)
3232

3333
const B2C_APP_ACCESS_RIGHT_FIELDS = `{ app { id } condoUserId condoUserEmail environment ${COMMON_FIELDS} ${EXPORT_FIELDS} }`

apps/dev-portal-api/domains/miniapp/plugins/exportable.js

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
11
const { userIsAdminOrIsSupport } = require('@open-condo/keystone/access')
22
const { plugin } = require('@open-condo/keystone/plugins/utils/typing')
33

4-
const { AVAILABLE_ENVIRONMENTS } = require('@dev-portal-api/domains/miniapp/constants/publishing')
4+
const { getEnvironmentalFields } = require('@dev-portal-api/domains/miniapp/schema/fields/environmental')
5+
6+
const BASE_FIELD = {
7+
type: 'Text',
8+
schemaDoc:
9+
'ID of this entity in the {environment} environment. ' +
10+
'If set, subsequent publications to this environment will update the entity with the specified ID.',
11+
isRequired: false,
12+
access: {
13+
read: true,
14+
create: userIsAdminOrIsSupport,
15+
update: userIsAdminOrIsSupport,
16+
},
17+
}
518

619
function exportable () {
720
return plugin(({ fields = {}, ...rest }) => {
8-
for (const environment of AVAILABLE_ENVIRONMENTS) {
9-
const fieldName = `${environment}ExportId`
10-
fields[fieldName] = {
11-
type: 'Text',
12-
schemaDoc:
13-
`ID of this entity in the ${environment} environment. ` +
14-
'If set, subsequent publications to this environment will update the entity with the specified ID.',
15-
isRequired: false,
16-
access: {
17-
read: true,
18-
create: userIsAdminOrIsSupport,
19-
update: userIsAdminOrIsSupport,
20-
},
21-
}
22-
}
23-
24-
return { fields, ...rest }
21+
return { fields: { ...fields, ...getEnvironmentalFields('exportId', BASE_FIELD) }, ...rest }
2522
})
2623
}
2724

apps/dev-portal-api/domains/miniapp/schema/B2CApp.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Generated by `createschema miniapp.B2CApp 'name:Text'`
33
*/
44

5+
const { userIsAdminOrIsSupport } = require('@open-condo/keystone/access')
56
const { historical, versioned, uuided, tracked, softDeleted, dvAndSender, analytical } = require('@open-condo/keystone/plugins')
67
const { GQLListSchema } = require('@open-condo/keystone/schema')
78

@@ -11,9 +12,12 @@ const {
1112
FileAdapter,
1213
getMimeTypesValidator,
1314
} = require('@dev-portal-api/domains/common/utils/files')
15+
const { B2C_APP_TYPES, B2C_APP_CORDOVA_TYPE } = require('@dev-portal-api/domains/miniapp/constants/b2c')
1416
const { exportable } = require('@dev-portal-api/domains/miniapp/plugins/exportable')
1517
const { canReadAppSchemas, canManageAppSchemas } = require('@dev-portal-api/domains/miniapp/utils/serverSchema/access')
1618

19+
const { getEnvironmentalFields } = require('./fields/environmental')
20+
1721
const LOGO_FILE_ADAPTER = new FileAdapter('B2CApps/logos', true)
1822
const LOGO_META_AFTER_CHANGE = getFileMetaAfterChange(LOGO_FILE_ADAPTER, 'logo')
1923

@@ -25,6 +29,19 @@ const B2CApp = new GQLListSchema('B2CApp', {
2529
type: 'Text',
2630
isRequired: true,
2731
},
32+
type: {
33+
schemaDoc: 'Type of application',
34+
type: 'Select',
35+
dataType: 'string',
36+
isRequired: true,
37+
options: B2C_APP_TYPES,
38+
defaultValue: B2C_APP_CORDOVA_TYPE,
39+
access: {
40+
read: true,
41+
create: true,
42+
update: userIsAdminOrIsSupport,
43+
},
44+
},
2845
logo: {
2946
schemaDoc: 'Icon of application',
3047
type: 'File',
@@ -39,6 +56,25 @@ const B2CApp = new GQLListSchema('B2CApp', {
3956
type: 'Text',
4057
isRequired: false,
4158
},
59+
...getEnvironmentalFields('webTransformEnabled', {
60+
schemaDoc:
61+
'Enables automatic transformation from native environments to web ' +
62+
'and allows automatic web-hosting on publish for {environment} environment',
63+
type: 'Checkbox',
64+
isRequired: true,
65+
defaultValue: false,
66+
}),
67+
...getEnvironmentalFields('publishedAt', {
68+
schemaDoc: 'The last time a mini-app was published on the {environment} environment',
69+
type: 'DateTimeUtc',
70+
kmigratorOptions: { db_index: true },
71+
access: {
72+
read: true,
73+
create: false,
74+
// NOTE: function used to make it appear in B2CAppUpdateInput
75+
update: () => false,
76+
},
77+
}),
4278
},
4379
hooks: {
4480
validateInput: getSharedConstraintsValidator(['B2BApp']),

apps/dev-portal-api/domains/miniapp/schema/B2CApp.test.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ const {
1414
expectToThrowAuthenticationErrorToObjects,
1515
expectToThrowAccessDeniedErrorToObj,
1616
expectToThrowGQLError,
17+
expectToThrowGraphQLRequestError,
1718
UploadingFile,
1819
} = require('@open-condo/keystone/test.utils')
1920

2021
const { INVALID_MIMETYPE } = require('@dev-portal-api/domains/common/constants/errors')
22+
const { B2C_APP_CORDOVA_TYPE, B2C_APP_WEB_TYPE } = require('@dev-portal-api/domains/miniapp/constants/b2c')
23+
const { AVAILABLE_ENVIRONMENTS } = require('@dev-portal-api/domains/miniapp/constants/publishing')
24+
const { getEnvironmentalFieldName } = require('@dev-portal-api/domains/miniapp/schema/fields/environmental')
2125
const {
2226
B2CApp,
2327
createTestB2CApp,
@@ -225,4 +229,77 @@ describe('B2CApp', () => {
225229
})
226230
})
227231
})
232+
describe('Field access tests', () => {
233+
let actors = {
234+
admin: null,
235+
support: null,
236+
user: null,
237+
anotherUser: null,
238+
}
239+
beforeAll(() => {
240+
actors = {
241+
admin,
242+
support,
243+
user,
244+
anotherUser,
245+
}
246+
})
247+
describe('type field', () => {
248+
let app
249+
beforeAll(async () => {
250+
[app] = await createTestB2CApp(user, { type: B2C_APP_CORDOVA_TYPE })
251+
})
252+
test('Can be changed after app creation by admin', async () => {
253+
const [updatedApp] = await updateTestB2CApp(admin, app.id, { type: B2C_APP_WEB_TYPE })
254+
expect(updatedApp).toHaveProperty('type', B2C_APP_WEB_TYPE)
255+
256+
const [revertedApp] = await updateTestB2CApp(admin, app.id, { type: B2C_APP_CORDOVA_TYPE })
257+
expect(revertedApp).toHaveProperty('type', B2C_APP_CORDOVA_TYPE)
258+
})
259+
test('Can be changed after app creation by support', async () => {
260+
const [updatedApp] = await updateTestB2CApp(support, app.id, { type: B2C_APP_WEB_TYPE })
261+
expect(updatedApp).toHaveProperty('type', B2C_APP_WEB_TYPE)
262+
263+
const [revertedApp] = await updateTestB2CApp(support, app.id, { type: B2C_APP_CORDOVA_TYPE })
264+
expect(revertedApp).toHaveProperty('type', B2C_APP_CORDOVA_TYPE)
265+
})
266+
test('Cannot be changed after app creation by app owner', async () => {
267+
await expectToThrowAccessDeniedErrorToObj(async () => {
268+
await updateTestB2CApp(user, app.id, { type: B2C_APP_WEB_TYPE })
269+
})
270+
})
271+
test('Cannot be changed after app creation by other user', async () => {
272+
await expectToThrowAccessDeniedErrorToObj(async () => {
273+
await updateTestB2CApp(anotherUser, app.id, { type: B2C_APP_WEB_TYPE })
274+
})
275+
})
276+
})
277+
describe.each(AVAILABLE_ENVIRONMENTS)('%p environment', (environment) => {
278+
const fieldName = getEnvironmentalFieldName(environment, 'publishedAt')
279+
describe(`${fieldName} field`, () => {
280+
describe('Cannot be manually set on creation', () => {
281+
test.each(Object.keys(actors))('By %p', async (actorName) => {
282+
const actor = actors[actorName]
283+
284+
await expectToThrowGraphQLRequestError(async () => {
285+
await createTestB2CApp(actor, { [fieldName]: dayjs().toISOString() })
286+
}, `Field "${fieldName}" is not defined by type "B2CAppCreateInput"`)
287+
})
288+
})
289+
describe('Cannot be manually set on update', () => {
290+
let app
291+
beforeAll(async () => {
292+
[app] = await createTestB2CApp(user)
293+
})
294+
test.each(Object.keys(actors))('By %p', async (actorName) => {
295+
const actor = actors[actorName]
296+
297+
await expectToThrowAccessDeniedErrorToObj(async () => {
298+
await updateTestB2CApp(actor, app.id, { [fieldName]: dayjs().toISOString() })
299+
})
300+
})
301+
})
302+
})
303+
})
304+
})
228305
})

apps/dev-portal-api/domains/miniapp/schema/PublishB2CAppService.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const {
2020
CondoB2CAppAccessRightGql,
2121
} = require('@dev-portal-api/domains/condo/gql')
2222
const access = require('@dev-portal-api/domains/miniapp/access/PublishB2CAppService')
23-
const { DEFAULT_COLOR_SCHEMA } = require('@dev-portal-api/domains/miniapp/constants/b2c')
23+
const { DEFAULT_COLOR_SCHEMA, B2C_APP_CORDOVA_TYPE } = require('@dev-portal-api/domains/miniapp/constants/b2c')
2424
const {
2525
FIRST_PUBLISH_WITHOUT_INFO,
2626
APP_NOT_FOUND,
@@ -41,8 +41,11 @@ const {
4141
B2CAppAccessRight,
4242
} = require('@dev-portal-api/domains/miniapp/utils/serverSchema/index')
4343

44+
const { getEnvironmentalFieldsSelection, getEnvironmentalFieldName } = require('./fields/environmental')
4445
const { getOIDCClientWhere } = require('./GetOIDCClientService')
4546

47+
const B2C_APP_FIELDS = `id name developer type createdBy { name } logo { publicUrl originalFilename } ${getEnvironmentalFieldsSelection(['exportId', 'webTransformEnabled'])}`
48+
4649
const ERRORS = {
4750
FIRST_PUBLISH_WITHOUT_INFO: {
4851
code: BAD_USER_INPUT,
@@ -82,6 +85,17 @@ function getExportIdField (environment) {
8285
return `${environment}ExportId`
8386
}
8487

88+
function getAppStaticUrl ({ condoApp, serverClient }) {
89+
const condoUrl = new URL(serverClient.endpoint)
90+
const condoDomainParts = condoUrl.hostname.split('.')
91+
// NOTE: condo.example.com -> example.com, localhost -> localhost, example.com -> example.com
92+
const domainSuffix = condoDomainParts.length > 2 ? condoDomainParts.slice(1) : condoDomainParts
93+
const domainParts = [condoApp.id, 'miniapps-static', ...domainSuffix]
94+
const port = condoUrl.port
95+
96+
return `${condoUrl.protocol}//${domainParts.join('.')}${port ? `:${port}` : ''}`
97+
}
98+
8599
async function publishAppChanges ({ app, condoApp, serverClient, args, context }) {
86100
const { data: { dv, sender, environment } } = args
87101
logger.info({
@@ -93,6 +107,9 @@ async function publishAppChanges ({ app, condoApp, serverClient, args, context }
93107
const exportIdField = getExportIdField(environment)
94108
const exportId = app[exportIdField]
95109

110+
const webTransformField = getEnvironmentalFieldName(environment, 'webTransformEnabled')
111+
const webTransformEnabled = app[webTransformField] === true
112+
96113
// Step 1. Prepare payload
97114
const appPayload = {
98115
dv,
@@ -123,6 +140,13 @@ async function publishAppChanges ({ app, condoApp, serverClient, args, context }
123140
environment,
124141
data: { condoAppId: condoApp.id },
125142
})
143+
144+
if (app.type === B2C_APP_CORDOVA_TYPE) {
145+
appPayload.appUrl = webTransformEnabled
146+
? getAppStaticUrl({ condoApp, serverClient })
147+
: null
148+
}
149+
126150
updatedCondoApp = await serverClient.updateModel({
127151
modelGql: CondoB2CAppGql,
128152
id: condoApp.id,
@@ -147,6 +171,17 @@ async function publishAppChanges ({ app, condoApp, serverClient, args, context }
147171
modelGql: CondoB2CAppGql,
148172
createInput: appPayload,
149173
})
174+
if (app.type === B2C_APP_CORDOVA_TYPE && webTransformEnabled) {
175+
updatedCondoApp = await serverClient.updateModel({
176+
modelGql: CondoB2CAppGql,
177+
id: updatedCondoApp.id,
178+
updateInput: {
179+
dv,
180+
sender,
181+
appUrl: getAppStaticUrl({ condoApp: updatedCondoApp, serverClient }),
182+
},
183+
})
184+
}
150185
logger.info({
151186
msg: 'condo app successfully created',
152187
entityId: app.id,
@@ -441,12 +476,15 @@ const PublishB2CAppService = new GQLCustomSchema('PublishB2CAppService', {
441476
access: access.canPublishB2CApp,
442477
schema: 'publishB2CApp(data: PublishB2CAppInput!): PublishB2CAppOutput',
443478
resolver: async (parent, args, context) => {
444-
const { data: { app: { id }, options, environment } } = args
479+
const { data: { app: { id }, options, environment, dv, sender } } = args
480+
481+
const publishingTime = dayjs().toISOString()
482+
const publishingField = getEnvironmentalFieldName(environment, 'publishedAt')
445483

446484
const app = await B2CApp.getOne(
447485
context,
448486
{ id, deletedAt: null },
449-
'id developmentExportId productionExportId name developer createdBy { name } logo { publicUrl originalFilename }'
487+
B2C_APP_FIELDS,
450488
)
451489
if (!app) {
452490
throw new GQLError(ERRORS.APP_NOT_FOUND, context)
@@ -530,6 +568,12 @@ const PublishB2CAppService = new GQLCustomSchema('PublishB2CAppService', {
530568
// Step 4. If OIDC client was created, publish must enable it for usage
531569
await syncOIDCClient({ args, serverClient, condoApp })
532570

571+
await B2CApp.update(context, app.id, {
572+
dv,
573+
sender,
574+
[publishingField]: publishingTime,
575+
})
576+
533577
return {
534578
success: true,
535579
}

0 commit comments

Comments
 (0)