@appsemble/node-utils 0.25.2 → 0.27.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 +3 -3
- package/app.d.ts +1 -0
- package/app.js +181 -0
- package/package.json +9 -5
- package/resource.js +0 -1
- package/server/controllers/resources.d.ts +2 -0
- package/server/controllers/resources.js +117 -75
- package/server/controllers/resources.test.js +0 -6
- package/server/routes/appRouter/index.js +1 -1
- package/server/routes/appRouter/indexHandler.js +1 -0
- package/server/routes/appRouter/robotsHandler.d.ts +2 -1
- package/server/routes/appRouter/robotsHandler.js +17 -3
- package/server/types.d.ts +1 -5
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#  Appsemble Node Utilities
|
|
2
2
|
|
|
3
3
|
> NodeJS utilities used by Appsemble internally.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@appsemble/node-utils)
|
|
6
|
-
[](https://gitlab.com/appsemble/appsemble/-/releases/0.27.0)
|
|
7
7
|
[](https://prettier.io)
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
@@ -26,5 +26,5 @@ compatibility is not guaranteed.
|
|
|
26
26
|
|
|
27
27
|
## License
|
|
28
28
|
|
|
29
|
-
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.
|
|
29
|
+
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.27.0/LICENSE.md) ©
|
|
30
30
|
[Appsemble](https://appsemble.com)
|
package/app.d.ts
CHANGED
|
@@ -15,3 +15,4 @@ import { type Options } from './server/types.js';
|
|
|
15
15
|
* @returns A localized remapper context for the app.
|
|
16
16
|
*/
|
|
17
17
|
export declare function getRemapperContext(app: App, language: string, userInfo: UserInfo, options: Options, context: ParameterizedContext<DefaultState, DefaultContext, any>): Promise<RemapperContext>;
|
|
18
|
+
export declare function applyAppVariant(appPath: string, appVariant: string): Promise<void>;
|
package/app.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
1
4
|
import { defaultLocale, has, objectCache } from '@appsemble/utils';
|
|
2
5
|
import { memoize } from '@formatjs/fast-memoize';
|
|
6
|
+
import { generate, parse, walk } from 'css-tree';
|
|
7
|
+
import { copy, ensureDir } from 'fs-extra';
|
|
3
8
|
import { IntlMessageFormat } from 'intl-messageformat';
|
|
9
|
+
import lodash from 'lodash';
|
|
10
|
+
import { format, resolveConfig } from 'prettier';
|
|
11
|
+
import { parseDocument } from 'yaml';
|
|
12
|
+
import { readData, writeData } from './fs.js';
|
|
13
|
+
import { logger } from './logger.js';
|
|
4
14
|
const getNumberFormat = memoize((locale, opts) => new Intl.NumberFormat(locale, opts));
|
|
5
15
|
const getPluralRules = memoize((locale, opts) => new Intl.PluralRules(locale, opts));
|
|
6
16
|
/**
|
|
@@ -47,4 +57,175 @@ export async function getRemapperContext(app, language, userInfo, options, conte
|
|
|
47
57
|
locale: (_b = (_a = userInfo === null || userInfo === void 0 ? void 0 : userInfo.locale) !== null && _a !== void 0 ? _a : app.definition.defaultLanguage) !== null && _b !== void 0 ? _b : defaultLocale,
|
|
48
58
|
};
|
|
49
59
|
}
|
|
60
|
+
function replaceStyle(stylesheet, selector, property, value) {
|
|
61
|
+
try {
|
|
62
|
+
walk(stylesheet, (nodeSelector) => {
|
|
63
|
+
if (nodeSelector.type !== 'Rule' || nodeSelector.prelude.type !== 'Raw') {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (nodeSelector.prelude.value === selector) {
|
|
67
|
+
walk(nodeSelector, (declaration) => {
|
|
68
|
+
if (declaration.type !== 'Declaration' || declaration.value.type !== 'Raw') {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (declaration.property === property) {
|
|
72
|
+
// eslint-disable-next-line no-param-reassign
|
|
73
|
+
declaration.value.value = value;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return stylesheet;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
logger.error(error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Patch an app definition of an app inside the `apps` folder.
|
|
86
|
+
*
|
|
87
|
+
* @param appPath The path to the app to patch.
|
|
88
|
+
* @param patches Patches to apply to the YAML.
|
|
89
|
+
*/
|
|
90
|
+
async function patchDefinition(appPath, patches) {
|
|
91
|
+
try {
|
|
92
|
+
const path = join(appPath, 'app-definition.yaml');
|
|
93
|
+
logger.verbose(`Updating ${path}`);
|
|
94
|
+
const yaml = await readFile(path, 'utf8');
|
|
95
|
+
const doc = parseDocument(yaml);
|
|
96
|
+
for (const [key, value] of patches) {
|
|
97
|
+
if (value === undefined) {
|
|
98
|
+
doc.deleteIn(key);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
doc.setIn(key, value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const prettierOptions = (await resolveConfig(path, { editorconfig: true }));
|
|
105
|
+
prettierOptions.parser = 'yaml';
|
|
106
|
+
await writeFile(path, await format(String(doc), prettierOptions));
|
|
107
|
+
logger.verbose(`Successfully updated ${path}`);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
logger.error(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Patch app messages.
|
|
115
|
+
*
|
|
116
|
+
* The replacements will be deep merged into the original content.
|
|
117
|
+
*
|
|
118
|
+
* @param appPath The name of the app to patch.
|
|
119
|
+
* @param replacements Replacements for the original messages.
|
|
120
|
+
*/
|
|
121
|
+
async function patchMessages(appPath, replacements) {
|
|
122
|
+
try {
|
|
123
|
+
const i18nPath = join(appPath, 'i18n');
|
|
124
|
+
for (const [language, replacementMessages] of Object.entries(replacements)) {
|
|
125
|
+
const originalMessagesPath = join(i18nPath, `${language}.json`);
|
|
126
|
+
logger.verbose(`Updating ${originalMessagesPath}`);
|
|
127
|
+
const [originalMessages] = await readData(originalMessagesPath);
|
|
128
|
+
await (replacementMessages
|
|
129
|
+
? writeData(originalMessagesPath, lodash.merge(originalMessages, replacementMessages))
|
|
130
|
+
: rm(originalMessagesPath));
|
|
131
|
+
logger.verbose(`Successfully updated ${originalMessagesPath}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
logger.error(error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function patchStyle(replacementsPath, replacements) {
|
|
139
|
+
try {
|
|
140
|
+
logger.verbose(`Updating ${replacementsPath}`);
|
|
141
|
+
const cssFile = await readFile(replacementsPath, 'utf8');
|
|
142
|
+
let doc = parse(cssFile, {
|
|
143
|
+
parseAtrulePrelude: false,
|
|
144
|
+
parseRulePrelude: false,
|
|
145
|
+
parseValue: false,
|
|
146
|
+
});
|
|
147
|
+
for (const { property, selector, value } of replacements) {
|
|
148
|
+
doc = replaceStyle(doc, selector, property, String(value));
|
|
149
|
+
}
|
|
150
|
+
await writeFile(replacementsPath, generate(doc));
|
|
151
|
+
logger.verbose(`Successfully updated ${replacementsPath}`);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
logger.error(error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Patch the app's styles.
|
|
159
|
+
*
|
|
160
|
+
* @param appPath The path to the app to patch.
|
|
161
|
+
* @param replacements Replacements for the original style.
|
|
162
|
+
*/
|
|
163
|
+
async function patchStyles(appPath, replacements) {
|
|
164
|
+
try {
|
|
165
|
+
const themePath = join(appPath, 'theme');
|
|
166
|
+
// Style type can be @appsemble, shared or core
|
|
167
|
+
for (const [styleType, replacementsByStyleType] of Object.entries(replacements)) {
|
|
168
|
+
const styleTypePath = join(themePath, styleType);
|
|
169
|
+
// FileOrBlock name can either be the name of a css file or the name of an appsemble block
|
|
170
|
+
for (const [fileOrBlockName, replacementsByFileOrBlock] of Object.entries(replacementsByStyleType)) {
|
|
171
|
+
const fileOrBlockPath = join(styleTypePath, fileOrBlockName);
|
|
172
|
+
if (Array.isArray(replacementsByFileOrBlock)) {
|
|
173
|
+
await patchStyle(fileOrBlockPath, replacementsByFileOrBlock);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
for (const [blockFilename, replacementsByFile] of Object.entries(replacementsByFileOrBlock)) {
|
|
177
|
+
const blockFilePath = join(fileOrBlockPath, blockFilename);
|
|
178
|
+
await patchStyle(blockFilePath, replacementsByFile);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
logger.error(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function transferAppVariantFiles(appPath, appVariantDefDir, appVariantDestDir, pathToCopy) {
|
|
189
|
+
try {
|
|
190
|
+
const defPathToCopy = join(appVariantDefDir, pathToCopy);
|
|
191
|
+
if (existsSync(defPathToCopy)) {
|
|
192
|
+
const existingPath = join(appVariantDestDir, pathToCopy);
|
|
193
|
+
if (existsSync(existingPath)) {
|
|
194
|
+
await rm(existingPath, { recursive: true });
|
|
195
|
+
}
|
|
196
|
+
await copy(defPathToCopy, join(appVariantDestDir, pathToCopy));
|
|
197
|
+
logger.verbose(`Successfully copied ${pathToCopy} to ${appVariantDestDir}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
logger.error(error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export async function applyAppVariant(appPath, appVariant) {
|
|
205
|
+
try {
|
|
206
|
+
logger.info(`Applying ${appVariant} variant to ${appPath}`);
|
|
207
|
+
const appVariantDestDir = join(dirname(appPath), `${basename(appPath)}-${appVariant}`);
|
|
208
|
+
await ensureDir(appVariantDestDir);
|
|
209
|
+
logger.verbose(`Successfully created ${appVariantDestDir}`);
|
|
210
|
+
await copy(appPath, appVariantDestDir);
|
|
211
|
+
logger.verbose(`Successfully copied app files to ${appVariantDestDir}`);
|
|
212
|
+
const appVariantDefDir = join(appPath, 'variants', appVariant);
|
|
213
|
+
await transferAppVariantFiles(appPath, appVariantDefDir, appVariantDestDir, 'assets');
|
|
214
|
+
await transferAppVariantFiles(appPath, appVariantDefDir, appVariantDestDir, 'resources');
|
|
215
|
+
await transferAppVariantFiles(appPath, appVariantDefDir, appVariantDestDir, 'screenshots');
|
|
216
|
+
await transferAppVariantFiles(appPath, appVariantDefDir, appVariantDestDir, 'icon.png');
|
|
217
|
+
const patchesPath = join(appVariantDefDir, 'patches');
|
|
218
|
+
const [definitionPatches] = await readData(join(patchesPath, 'app-definition.json'));
|
|
219
|
+
await patchDefinition(appVariantDestDir, definitionPatches);
|
|
220
|
+
const [messagesPatches] = await readData(join(patchesPath, 'messages.json'));
|
|
221
|
+
await patchMessages(appVariantDestDir, messagesPatches);
|
|
222
|
+
const [stylesPatches] = await readData(join(patchesPath, 'styles.json'));
|
|
223
|
+
await patchStyles(appVariantDestDir, stylesPatches);
|
|
224
|
+
logger.info(`Successfully applied ${appVariant} variant to ${appPath}`);
|
|
225
|
+
logger.verbose(`The ${appVariant} variant app can be found in ${appVariantDestDir}`);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
logger.error(error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
50
231
|
//# sourceMappingURL=app.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/node-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "NodeJS utilities used by Appsemble internally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"test": "vitest"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@appsemble/types": "0.
|
|
42
|
-
"@appsemble/utils": "0.
|
|
41
|
+
"@appsemble/types": "0.27.0",
|
|
42
|
+
"@appsemble/utils": "0.27.0",
|
|
43
43
|
"@formatjs/fast-memoize": "^2.0.0",
|
|
44
44
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
|
45
45
|
"@koa/cors": "^4.0.0",
|
|
@@ -52,13 +52,17 @@
|
|
|
52
52
|
"bulma-switch": "^2.0.0",
|
|
53
53
|
"chalk": "^5.0.0",
|
|
54
54
|
"cli-highlight": "^2.0.0",
|
|
55
|
+
"css-tree": "^2.0.0",
|
|
55
56
|
"csv-parse": "^5.0.0",
|
|
56
57
|
"date-fns": "^2.0.0",
|
|
57
58
|
"express-to-koa": "^2.0.0",
|
|
58
59
|
"fast-glob": "^3.0.0",
|
|
59
60
|
"form-data": "^4.0.0",
|
|
61
|
+
"fs-extra": "^11.0.0",
|
|
62
|
+
"inquirer": "^9.0.0",
|
|
60
63
|
"intl-messageformat": "^10.0.0",
|
|
61
64
|
"jsonschema": "^1.0.0",
|
|
65
|
+
"keytar": "^7.0.0",
|
|
62
66
|
"koa": "^2.0.0",
|
|
63
67
|
"koa-compose": "^4.0.0",
|
|
64
68
|
"koa-compress": "^5.0.0",
|
|
@@ -74,6 +78,7 @@
|
|
|
74
78
|
"koas-status-code": "^0.7.0",
|
|
75
79
|
"koas-swagger-ui": "^0.7.0",
|
|
76
80
|
"language-tags": "^1.0.0",
|
|
81
|
+
"lodash": "^4.0.0",
|
|
77
82
|
"lodash-es": "^4.0.0",
|
|
78
83
|
"logform": "^2.0.0",
|
|
79
84
|
"mime-types": "^2.0.0",
|
|
@@ -88,12 +93,11 @@
|
|
|
88
93
|
"webpack": "^5.0.0",
|
|
89
94
|
"webpack-dev-middleware": "^6.0.0",
|
|
90
95
|
"winston": "^3.0.0",
|
|
91
|
-
"keytar": "^7.0.0",
|
|
92
|
-
"inquirer": "^9.0.0",
|
|
93
96
|
"yaml": "^2.0.0",
|
|
94
97
|
"yargs": "^17.0.0"
|
|
95
98
|
},
|
|
96
99
|
"devDependencies": {
|
|
100
|
+
"@types/css-tree": "^2.0.0",
|
|
97
101
|
"@types/koa-compress": "^4.0.0",
|
|
98
102
|
"@types/koa-range": "^0.3.0",
|
|
99
103
|
"axios-mock-adapter": "^1.0.0",
|
package/resource.js
CHANGED
|
@@ -146,7 +146,6 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
|
|
|
146
146
|
],
|
|
147
147
|
},
|
|
148
148
|
$clonable: { type: 'boolean' },
|
|
149
|
-
$ephemeral: { type: 'boolean' },
|
|
150
149
|
...Object.fromEntries(Object.values((_a = definition.references) !== null && _a !== void 0 ? _a : {}).map((reference) => [
|
|
151
150
|
`$${reference.resource}`,
|
|
152
151
|
{ type: 'integer' },
|
|
@@ -6,3 +6,5 @@ export declare function createGetResourceById(options: Options): Middleware;
|
|
|
6
6
|
export declare function createCreateResource(options: Options): Middleware;
|
|
7
7
|
export declare function createUpdateResource(options: Options): Middleware;
|
|
8
8
|
export declare function createDeleteResource(options: Options): Middleware;
|
|
9
|
+
export declare function createSeedResource(options: Options): Middleware;
|
|
10
|
+
export declare function createDeleteSeedResources(options: Options): Middleware;
|
|
@@ -159,10 +159,9 @@ export function createGetResourceById(options) {
|
|
|
159
159
|
export function createCreateResource(options) {
|
|
160
160
|
return async (ctx) => {
|
|
161
161
|
const { pathParams: { appId, resourceType }, } = ctx;
|
|
162
|
-
const {
|
|
162
|
+
const { createAppResourcesWithAssets, getApp, getAppAssets, verifyResourceActionPermission } = options;
|
|
163
163
|
const action = 'create';
|
|
164
164
|
const app = await getApp({ context: ctx, query: { where: { id: appId } } });
|
|
165
|
-
const resourceSeeded = await checkSeededResources({ context: ctx, app, resourceType });
|
|
166
165
|
const resourceDefinition = getResourceDefinition(app, resourceType, ctx);
|
|
167
166
|
await verifyResourceActionPermission({ app, context: ctx, action, resourceType, options, ctx });
|
|
168
167
|
const appAssets = await getAppAssets({ app, context: ctx });
|
|
@@ -172,78 +171,19 @@ export function createCreateResource(options) {
|
|
|
172
171
|
return;
|
|
173
172
|
}
|
|
174
173
|
const resources = Array.isArray(processedBody) ? processedBody : [processedBody];
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
for (const referencedProperty of Object.keys((_a = resourceDefinition.references) !== null && _a !== void 0 ? _a : {})) {
|
|
189
|
-
delete cleanResource[referencedProperty];
|
|
190
|
-
}
|
|
191
|
-
return {
|
|
192
|
-
...cleanResource,
|
|
193
|
-
$seed: true,
|
|
194
|
-
$ephemeral: false,
|
|
195
|
-
};
|
|
196
|
-
});
|
|
197
|
-
for (const preparedSeedAsset of preparedSeedAssets) {
|
|
198
|
-
const index = resources.findIndex(({ $clonable: clonable, $ephemeral: ephemeral, $seed: seed, ...cleanResource }) => {
|
|
199
|
-
const { $clonable, $ephemeral, $seed, ...cleanAssetResource } = preparedSeedAsset.resource;
|
|
200
|
-
return isDeepStrictEqual(cleanResource, cleanAssetResource);
|
|
201
|
-
});
|
|
202
|
-
const previousAssetId = preparedSeedAsset.id;
|
|
203
|
-
const newAssetId = randomUUID();
|
|
204
|
-
const updatedResource = JSON.parse(JSON.stringify(preparedSeedResources[index]).replace(previousAssetId, newAssetId));
|
|
205
|
-
preparedSeedAsset.id = newAssetId;
|
|
206
|
-
preparedSeedAsset.resource = updatedResource;
|
|
207
|
-
preparedSeedResources[index] = updatedResource;
|
|
208
|
-
}
|
|
209
|
-
await createAppResourcesWithAssets({
|
|
210
|
-
app,
|
|
211
|
-
context: ctx,
|
|
212
|
-
resources: preparedSeedResources,
|
|
213
|
-
preparedAssets: preparedSeedAssets,
|
|
214
|
-
resourceType,
|
|
215
|
-
action,
|
|
216
|
-
options,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
createdResources = await createAppResourcesWithAssets({
|
|
220
|
-
app,
|
|
221
|
-
context: ctx,
|
|
222
|
-
resources: resources.map((resource) => ({
|
|
223
|
-
...resource,
|
|
224
|
-
$seed: false,
|
|
225
|
-
$ephemeral: true,
|
|
226
|
-
$clonable: false,
|
|
227
|
-
})),
|
|
228
|
-
preparedAssets,
|
|
229
|
-
resourceType,
|
|
230
|
-
action,
|
|
231
|
-
options,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
// In regular apps, resources are created as usual
|
|
236
|
-
// with "clonable", "ephemeral" and "expires" passed from the request body
|
|
237
|
-
createdResources = await createAppResourcesWithAssets({
|
|
238
|
-
app,
|
|
239
|
-
context: ctx,
|
|
240
|
-
resources,
|
|
241
|
-
preparedAssets,
|
|
242
|
-
resourceType,
|
|
243
|
-
action,
|
|
244
|
-
options,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
174
|
+
const createdResources = await createAppResourcesWithAssets({
|
|
175
|
+
app,
|
|
176
|
+
context: ctx,
|
|
177
|
+
resources: resources.map((resource) => ({
|
|
178
|
+
...resource,
|
|
179
|
+
$seed: false,
|
|
180
|
+
$ephemeral: app.demoMode,
|
|
181
|
+
})),
|
|
182
|
+
preparedAssets,
|
|
183
|
+
resourceType,
|
|
184
|
+
action,
|
|
185
|
+
options,
|
|
186
|
+
});
|
|
247
187
|
ctx.body = Array.isArray(processedBody) ? createdResources : createdResources[0];
|
|
248
188
|
};
|
|
249
189
|
}
|
|
@@ -337,10 +277,112 @@ export function createDeleteResource(options) {
|
|
|
337
277
|
context: ctx,
|
|
338
278
|
id: resourceId,
|
|
339
279
|
type: resourceType,
|
|
340
|
-
action,
|
|
341
280
|
options,
|
|
342
281
|
});
|
|
343
282
|
ctx.status = 204;
|
|
344
283
|
};
|
|
345
284
|
}
|
|
285
|
+
export function createSeedResource(options) {
|
|
286
|
+
return async (ctx) => {
|
|
287
|
+
const { pathParams: { appId, resourceType }, } = ctx;
|
|
288
|
+
const { createAppResourcesWithAssets, getApp, getAppAssets, verifyResourceActionPermission } = options;
|
|
289
|
+
const action = 'create';
|
|
290
|
+
const app = await getApp({ context: ctx, query: { where: { id: appId } } });
|
|
291
|
+
const resourceDefinition = getResourceDefinition(app, resourceType, ctx);
|
|
292
|
+
await verifyResourceActionPermission({ app, context: ctx, action, resourceType, options, ctx });
|
|
293
|
+
const appAssets = await getAppAssets({ app, context: ctx });
|
|
294
|
+
const [processedBody, preparedAssets] = processResourceBody(ctx, resourceDefinition, undefined, undefined, appAssets.map((appAsset) => ({ id: appAsset.id, name: appAsset.name })));
|
|
295
|
+
if (Array.isArray(processedBody) && !processedBody.length) {
|
|
296
|
+
ctx.body = [];
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const resources = Array.isArray(processedBody) ? processedBody : [processedBody];
|
|
300
|
+
let createdResources;
|
|
301
|
+
const preparedSeedAssets = structuredClone(preparedAssets);
|
|
302
|
+
const preparedSeedResources = resources.map((resource) => {
|
|
303
|
+
var _a;
|
|
304
|
+
const cleanResource = { ...resource };
|
|
305
|
+
if (app.demoMode) {
|
|
306
|
+
for (const referencedProperty of Object.keys((_a = resourceDefinition.references) !== null && _a !== void 0 ? _a : {})) {
|
|
307
|
+
delete cleanResource[referencedProperty];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
...cleanResource,
|
|
312
|
+
$seed: true,
|
|
313
|
+
$ephemeral: false,
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
for (const preparedSeedAsset of preparedSeedAssets) {
|
|
317
|
+
const index = resources.findIndex(({ $clonable: clonable, $ephemeral: ephemeral, $seed: seed, ...cleanResource }) => {
|
|
318
|
+
const { $clonable, $ephemeral, $seed, ...cleanAssetResource } = preparedSeedAsset.resource;
|
|
319
|
+
return isDeepStrictEqual(cleanResource, cleanAssetResource);
|
|
320
|
+
});
|
|
321
|
+
const previousAssetId = preparedSeedAsset.id;
|
|
322
|
+
const newAssetId = randomUUID();
|
|
323
|
+
const updatedResource = JSON.parse(JSON.stringify(preparedSeedResources[index]).replace(previousAssetId, newAssetId));
|
|
324
|
+
preparedSeedAsset.id = newAssetId;
|
|
325
|
+
preparedSeedAsset.resource = updatedResource;
|
|
326
|
+
preparedSeedResources[index] = updatedResource;
|
|
327
|
+
}
|
|
328
|
+
createdResources = await createAppResourcesWithAssets({
|
|
329
|
+
app,
|
|
330
|
+
context: ctx,
|
|
331
|
+
resources: preparedSeedResources,
|
|
332
|
+
preparedAssets: preparedSeedAssets,
|
|
333
|
+
resourceType,
|
|
334
|
+
action,
|
|
335
|
+
options,
|
|
336
|
+
});
|
|
337
|
+
if (app.demoMode) {
|
|
338
|
+
createdResources = await createAppResourcesWithAssets({
|
|
339
|
+
app,
|
|
340
|
+
context: ctx,
|
|
341
|
+
resources: resources.map((resource) => ({
|
|
342
|
+
...resource,
|
|
343
|
+
$seed: false,
|
|
344
|
+
$ephemeral: true,
|
|
345
|
+
$clonable: false,
|
|
346
|
+
})),
|
|
347
|
+
preparedAssets,
|
|
348
|
+
resourceType,
|
|
349
|
+
action,
|
|
350
|
+
options,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
ctx.body = Array.isArray(processedBody) ? createdResources : createdResources[0];
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
export function createDeleteSeedResources(options) {
|
|
357
|
+
return async (ctx) => {
|
|
358
|
+
var _a;
|
|
359
|
+
const { pathParams: { appId }, } = ctx;
|
|
360
|
+
const { deleteAppResource, getApp, getAppResources } = options;
|
|
361
|
+
const app = await getApp({ context: ctx, query: { where: { id: appId } } });
|
|
362
|
+
for (const resourceType of Object.keys((_a = app.definition.resources) !== null && _a !== void 0 ? _a : {})) {
|
|
363
|
+
const resourcesToDeleteFindOptions = {
|
|
364
|
+
where: {
|
|
365
|
+
type: resourceType,
|
|
366
|
+
AppId: appId,
|
|
367
|
+
or: [{ seed: true }, { ephemeral: true }],
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
const resourcesToDelete = await getAppResources({
|
|
371
|
+
app,
|
|
372
|
+
findOptions: resourcesToDeleteFindOptions,
|
|
373
|
+
type: resourceType,
|
|
374
|
+
context: ctx,
|
|
375
|
+
});
|
|
376
|
+
for (const resourceToDelete of resourcesToDelete) {
|
|
377
|
+
await deleteAppResource({
|
|
378
|
+
app,
|
|
379
|
+
context: ctx,
|
|
380
|
+
options,
|
|
381
|
+
id: resourceToDelete.id,
|
|
382
|
+
type: resourceType,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
346
388
|
//# sourceMappingURL=resources.js.map
|
|
@@ -11,7 +11,6 @@ let mockParseQuery;
|
|
|
11
11
|
let mockGetAppUrl;
|
|
12
12
|
let mockGetAppMessages;
|
|
13
13
|
let mockCreateAppResourcesWithAssets;
|
|
14
|
-
let mockCheckSeededResources;
|
|
15
14
|
let mockGetAppAssets;
|
|
16
15
|
let mockCtx;
|
|
17
16
|
let mockCtxIs;
|
|
@@ -429,7 +428,6 @@ describe('createCreateResource', () => {
|
|
|
429
428
|
mockGetApp = vi.fn();
|
|
430
429
|
mockVerifyResourceActionPermission = vi.fn();
|
|
431
430
|
mockCreateAppResourcesWithAssets = vi.fn();
|
|
432
|
-
mockCheckSeededResources = vi.fn();
|
|
433
431
|
mockGetAppAssets = vi.fn();
|
|
434
432
|
mockCtxIs = vi.fn();
|
|
435
433
|
mockCtx = {
|
|
@@ -453,13 +451,11 @@ describe('createCreateResource', () => {
|
|
|
453
451
|
mockGetApp.mockResolvedValue(mockApp);
|
|
454
452
|
mockCtxIs.mockReturnValue('multipart/form-data');
|
|
455
453
|
mockCreateAppResourcesWithAssets.mockResolvedValue(mockResources);
|
|
456
|
-
mockCheckSeededResources.mockResolvedValue(false);
|
|
457
454
|
mockGetAppAssets.mockResolvedValue([]);
|
|
458
455
|
const middleware = createCreateResource({
|
|
459
456
|
getApp: mockGetApp,
|
|
460
457
|
createAppResourcesWithAssets: mockCreateAppResourcesWithAssets,
|
|
461
458
|
verifyResourceActionPermission: mockVerifyResourceActionPermission,
|
|
462
|
-
checkSeededResources: mockCheckSeededResources,
|
|
463
459
|
getAppAssets: mockGetAppAssets,
|
|
464
460
|
});
|
|
465
461
|
await middleware(mockCtx, vi.fn());
|
|
@@ -514,13 +510,11 @@ describe('createCreateResource', () => {
|
|
|
514
510
|
mockGetApp.mockResolvedValue(mockApp);
|
|
515
511
|
mockCtxIs.mockReturnValue('multipart/form-data');
|
|
516
512
|
mockCreateAppResourcesWithAssets.mockResolvedValue(mockResources);
|
|
517
|
-
mockCheckSeededResources.mockResolvedValue(false);
|
|
518
513
|
mockGetAppAssets.mockResolvedValue([]);
|
|
519
514
|
const middleware = createCreateResource({
|
|
520
515
|
getApp: mockGetApp,
|
|
521
516
|
createAppResourcesWithAssets: mockCreateAppResourcesWithAssets,
|
|
522
517
|
verifyResourceActionPermission: mockVerifyResourceActionPermission,
|
|
523
|
-
checkSeededResources: mockCheckSeededResources,
|
|
524
518
|
getAppAssets: mockGetAppAssets,
|
|
525
519
|
});
|
|
526
520
|
await middleware(mockCtx, vi.fn());
|
|
@@ -52,6 +52,7 @@ export function createIndexHandler({ createSettings, getApp, getAppDetails, getA
|
|
|
52
52
|
const updated = app.$updated ? new Date(app.$updated) : new Date();
|
|
53
53
|
return render(ctx, 'app/index.html', {
|
|
54
54
|
app,
|
|
55
|
+
noIndex: app.visibility !== 'public',
|
|
55
56
|
appUrl: String(appUrl),
|
|
56
57
|
host,
|
|
57
58
|
locale: defaultLanguage,
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { logger } from '@appsemble/node-utils';
|
|
2
|
+
export function createRobotsHandler({ getApp }) {
|
|
3
|
+
return async (ctx) => {
|
|
4
|
+
const app = await getApp({ context: ctx });
|
|
5
|
+
switch (app.visibility) {
|
|
6
|
+
case 'public':
|
|
7
|
+
ctx.body = 'User-agent: *\nAllow: *\n';
|
|
8
|
+
break;
|
|
9
|
+
case 'unlisted':
|
|
10
|
+
case 'private':
|
|
11
|
+
ctx.body = 'User-agent: *\nDisallow: /\n';
|
|
12
|
+
break;
|
|
13
|
+
default:
|
|
14
|
+
logger.error('Unknown visibility', { visibility: app.visibility });
|
|
15
|
+
ctx.status = 500;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
4
18
|
};
|
|
5
19
|
}
|
|
6
20
|
//# sourceMappingURL=robotsHandler.js.map
|
package/server/types.d.ts
CHANGED
|
@@ -96,6 +96,7 @@ declare module 'koas-parameters' {
|
|
|
96
96
|
startIndex: number;
|
|
97
97
|
view: string;
|
|
98
98
|
resources: boolean;
|
|
99
|
+
assets: boolean;
|
|
99
100
|
roles: string[];
|
|
100
101
|
}
|
|
101
102
|
}
|
|
@@ -124,9 +125,6 @@ export interface GetAppSubEntityParams {
|
|
|
124
125
|
context: ParameterizedContext<DefaultState, DefaultContextInterface, any>;
|
|
125
126
|
app: App;
|
|
126
127
|
}
|
|
127
|
-
export interface CheckSeededResourcesParams extends GetAppSubEntityParams {
|
|
128
|
-
resourceType: string;
|
|
129
|
-
}
|
|
130
128
|
export interface GetAppMessagesParams extends GetAppSubEntityParams {
|
|
131
129
|
language?: string;
|
|
132
130
|
merge?: string[] | string;
|
|
@@ -273,7 +271,6 @@ export interface DeleteAppResourceParams extends GetAppSubEntityParams {
|
|
|
273
271
|
id: number | string;
|
|
274
272
|
type: string;
|
|
275
273
|
whereOptions?: WhereOptions;
|
|
276
|
-
action: ResourceAction;
|
|
277
274
|
options: Options;
|
|
278
275
|
}
|
|
279
276
|
export interface CreateAppAssetParams extends GetAppSubEntityParams {
|
|
@@ -375,6 +372,5 @@ export interface Options {
|
|
|
375
372
|
deleteAppAsset: (params: DeleteAppAssetParams) => Promise<number>;
|
|
376
373
|
email: (params: EmailParams) => Promise<void>;
|
|
377
374
|
sendNotifications: (params: SendNotificationsParams) => Promise<void>;
|
|
378
|
-
checkSeededResources: (params: CheckSeededResourcesParams) => Promise<boolean>;
|
|
379
375
|
}
|
|
380
376
|
export {};
|