@galadrim/galadmin 0.2.9 → 0.2.11-alpha.1.1
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/.agents/report/blockers.json +1 -0
- package/.agents/report/failures/2026-09-23-api-user-filter-contract.md +31 -0
- package/.agents/report/failures/2026-09-23-api-values-contract.md +25 -0
- package/.agents/report/failures/2026-10-01-cycle-02-create-callback-contract.md +17 -0
- package/.agents/report/failures/2026-10-01-cycle-06-delete-callback-id-contract.md +17 -0
- package/.agents/report/failures/2026-10-01-cycle-07-panel-delete-callback-id-contract.md +24 -0
- package/.agents/report/failures/2026-10-01-cycle-13-update-callback-id-contract.md +17 -0
- package/.agents/report/failures/2026-10-01-cycle-15-rich-text-upload-boundary-contract.md +24 -0
- package/.agents/report/failures/2026-10-03-cycle-06-record-import-parser-contract.md +11 -0
- package/.agents/report/failures/2026-10-03-cycle-07-panel-import-parser-contract.md +24 -0
- package/.agents/report/failures/2026-10-07-resume-cycle-02-api-assembly-contract.md +46 -0
- package/.agents/report/port/2026-09-23-api-config-enrichment.md +13 -0
- package/.agents/report/port/2026-09-23-api-file-upload.md +11 -0
- package/.agents/report/port/2026-09-23-api-query-authorization.md +14 -0
- package/.agents/report/port/2026-09-23-api-request-configuration.md +11 -0
- package/.agents/report/port/2026-09-23-database-manager.md +13 -0
- package/.agents/report/port/2026-10-01-api-adjacent-record-id.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-01-api-config-route.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-03-custom-action.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-04-custom-component.md +7 -0
- package/.agents/report/port/2026-10-01-cycle-05-custom-query.md +7 -0
- package/.agents/report/port/2026-10-01-cycle-08-bulk-delete.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-09-duplicate-record.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-10-notification-badges.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-11-record.md +7 -0
- package/.agents/report/port/2026-10-01-cycle-12-search-companies.md +11 -0
- package/.agents/report/port/2026-10-01-cycle-14-upload-file.md +7 -0
- package/.agents/report/port/2026-10-03-cycle-01-create-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-02-delete-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-03-panel-delete-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-04-update-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-05-api-values.md +11 -0
- package/.agents/report/port/2026-10-06-cycle-01-record-import.md +9 -0
- package/.agents/report/port/2026-10-06-cycle-03-rich-text-upload.md +7 -0
- package/.agents/report/port/2026-10-06-cycle-04-user-filters.md +11 -0
- package/.agents/report/port/2026-10-06-resume-cycle-01-panel-import.md +9 -0
- package/.agents/report/port/2026-10-07-cycle-01-api-assembly.md +16 -0
- package/.agents/report/port/2026-10-07-cycle-02-index-entry-point.md +13 -0
- package/.agents/report/remediation/2026-10-02-api-user-filter-remediation.md +71 -0
- package/.agents/report/remediation/2026-10-02-api-values-remediation.md +17 -0
- package/.agents/report/remediation/2026-10-03-callback-id-contract-remediation.md +36 -0
- package/.agents/report/remediation/2026-10-06-import-parser-remediation.md +41 -0
- package/.agents/report/remediation/2026-10-06-rich-text-upload-remediation.md +33 -0
- package/.agents/report/remediation/2026-10-07-api-assembly-remediation.md +32 -0
- package/.agents/skills/port-javascript-to-typescript/SKILL.md +42 -0
- package/.agents/skills/port-javascript-to-typescript/agents/openai.yaml +3 -0
- package/.agents/skills/port-javascript-to-typescript/references/blockers.md +31 -0
- package/.agents/skills/port-javascript-to-typescript/references/planning.md +43 -0
- package/.agents/skills/port-javascript-to-typescript/references/porting.md +9 -0
- package/.agents/skills/port-javascript-to-typescript/references/reporting.md +24 -0
- package/.agents/skills/port-javascript-to-typescript/references/rules.md +8 -0
- package/.agents/skills/port-javascript-to-typescript/references/selection.md +16 -0
- package/.agents/skills/port-javascript-to-typescript/references/tools.md +20 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/check.sh +17 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/graph.sh +30 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/madge.json +12 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/oxlint.json +14 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/test-tools.cjs +76 -0
- package/.github/actions/version-bump/action.yml +41 -0
- package/.github/workflows/alpha.yml +53 -0
- package/.github/workflows/ci.yml +36 -0
- package/.github/workflows/main.yml +14 -42
- package/AGENTS.md +5 -0
- package/README.md +41 -4
- package/dist/api/authorization.d.ts +38 -0
- package/dist/api/authorization.js +74 -0
- package/dist/api/authorization.js.map +1 -0
- package/dist/api/config.d.ts +42 -0
- package/dist/api/config.js +105 -0
- package/dist/api/config.js.map +1 -0
- package/dist/api/filterUser.d.ts +2 -0
- package/dist/api/filterUser.js +24 -0
- package/dist/api/filterUser.js.map +1 -0
- package/dist/api/importRecords.d.ts +1 -0
- package/dist/api/importRecords.js +16 -0
- package/dist/api/importRecords.js.map +1 -0
- package/dist/api/queries.d.ts +48 -0
- package/dist/api/queries.js +139 -0
- package/dist/api/queries.js.map +1 -0
- package/dist/api/request.d.ts +29 -0
- package/dist/api/request.js +3 -0
- package/dist/api/request.js.map +1 -0
- package/dist/api/routes/adjacentRecordId.d.ts +10 -0
- package/dist/api/routes/adjacentRecordId.js +13 -0
- package/dist/api/routes/adjacentRecordId.js.map +1 -0
- package/dist/api/routes/config.d.ts +3 -0
- package/dist/api/routes/config.js +27 -0
- package/dist/api/routes/config.js.map +1 -0
- package/dist/api/routes/createRecord.d.ts +19 -0
- package/dist/api/routes/createRecord.js +93 -0
- package/dist/api/routes/createRecord.js.map +1 -0
- package/dist/api/routes/customAction.d.ts +17 -0
- package/dist/api/routes/customAction.js +35 -0
- package/dist/api/routes/customAction.js.map +1 -0
- package/dist/api/routes/customComponent.d.ts +3 -0
- package/dist/api/routes/customComponent.js +34 -0
- package/dist/api/routes/customComponent.js.map +1 -0
- package/dist/api/routes/customQuery.d.ts +7 -0
- package/dist/api/routes/customQuery.js +33 -0
- package/dist/api/routes/customQuery.js.map +1 -0
- package/dist/api/routes/deleteRecord.d.ts +9 -0
- package/dist/api/routes/deleteRecord.js +46 -0
- package/dist/api/routes/deleteRecord.js.map +1 -0
- package/dist/api/routes/deleteRecordFromPanel.d.ts +10 -0
- package/dist/api/routes/deleteRecordFromPanel.js +36 -0
- package/dist/api/routes/deleteRecordFromPanel.js.map +1 -0
- package/dist/api/routes/deleteRecords.d.ts +9 -0
- package/dist/api/routes/deleteRecords.js +12 -0
- package/dist/api/routes/deleteRecords.js.map +1 -0
- package/dist/api/routes/duplicateRecord.d.ts +17 -0
- package/dist/api/routes/duplicateRecord.js +55 -0
- package/dist/api/routes/duplicateRecord.js.map +1 -0
- package/dist/api/routes/foreignSelectRecords.d.ts +10 -0
- package/dist/api/routes/foreignSelectRecords.js +61 -0
- package/dist/api/routes/foreignSelectRecords.js.map +1 -0
- package/dist/api/routes/notificationBadges.d.ts +6 -0
- package/dist/api/routes/notificationBadges.js +31 -0
- package/dist/api/routes/notificationBadges.js.map +1 -0
- package/dist/api/routes/record.d.ts +7 -0
- package/dist/api/routes/record.js +32 -0
- package/dist/api/routes/record.js.map +1 -0
- package/dist/api/routes/records.d.ts +14 -0
- package/dist/api/routes/records.js +57 -0
- package/dist/api/routes/records.js.map +1 -0
- package/dist/api/routes/recordsExport.d.ts +13 -0
- package/dist/api/routes/recordsExport.js +67 -0
- package/dist/api/routes/recordsExport.js.map +1 -0
- package/dist/api/routes/recordsImport.d.ts +4 -0
- package/dist/api/routes/recordsImport.js +100 -0
- package/dist/api/routes/recordsImport.js.map +1 -0
- package/dist/api/routes/searchCompanies.d.ts +17 -0
- package/dist/api/routes/searchCompanies.js +72 -0
- package/dist/api/routes/searchCompanies.js.map +1 -0
- package/dist/api/routes/subrecordsImport.d.ts +4 -0
- package/dist/api/routes/subrecordsImport.js +94 -0
- package/dist/api/routes/subrecordsImport.js.map +1 -0
- package/dist/api/routes/updateRecord.d.ts +13 -0
- package/dist/api/routes/updateRecord.js +55 -0
- package/dist/api/routes/updateRecord.js.map +1 -0
- package/dist/api/routes/uploadFile.d.ts +3 -0
- package/dist/api/routes/uploadFile.js +13 -0
- package/dist/api/routes/uploadFile.js.map +1 -0
- package/dist/api/routes/uploadFileFromBetterRichTextInput.d.ts +3 -0
- package/dist/api/routes/uploadFileFromBetterRichTextInput.js +33 -0
- package/dist/api/routes/uploadFileFromBetterRichTextInput.js.map +1 -0
- package/dist/api/uploadFile.d.ts +4 -0
- package/dist/api/uploadFile.js +209 -0
- package/dist/api/uploadFile.js.map +1 -0
- package/dist/api/values.d.ts +6 -0
- package/dist/api/values.js +88 -0
- package/dist/api/values.js.map +1 -0
- package/dist/api.d.ts +8 -27
- package/dist/api.js +80 -1389
- package/dist/api.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.strict.tsbuildinfo +1 -0
- package/dist/types/config.d.ts +17 -7
- package/dist/types/config.js +4 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/database.d.ts +1 -0
- package/dist/types/database.js +3 -0
- package/dist/types/database.js.map +1 -0
- package/dist/utils/dbManager.d.ts +32 -7
- package/dist/utils/dbManager.js +59 -7
- package/dist/utils/dbManager.js.map +1 -1
- package/package.json +12 -2
- package/src/api/authorization.ts +129 -0
- package/src/api/config.ts +152 -0
- package/src/api/filterUser.ts +28 -0
- package/src/api/importRecords.ts +16 -0
- package/src/api/queries.ts +194 -0
- package/src/api/request.ts +31 -0
- package/src/api/routes/adjacentRecordId.ts +20 -0
- package/src/api/routes/config.ts +15 -0
- package/src/api/routes/createRecord.ts +131 -0
- package/src/api/routes/customAction.ts +60 -0
- package/src/api/routes/customComponent.ts +44 -0
- package/src/api/routes/customQuery.ts +53 -0
- package/src/api/routes/deleteRecord.ts +59 -0
- package/src/api/routes/deleteRecordFromPanel.ts +58 -0
- package/src/api/routes/deleteRecords.ts +28 -0
- package/src/api/routes/duplicateRecord.ts +73 -0
- package/src/api/routes/foreignSelectRecords.ts +80 -0
- package/src/api/routes/notificationBadges.ts +47 -0
- package/src/api/routes/record.ts +48 -0
- package/src/api/routes/records.ts +91 -0
- package/src/api/routes/recordsExport.ts +92 -0
- package/src/api/routes/recordsImport.ts +115 -0
- package/src/api/routes/searchCompanies.ts +96 -0
- package/src/api/routes/subrecordsImport.ts +114 -0
- package/src/api/routes/updateRecord.ts +74 -0
- package/src/api/routes/uploadFile.ts +16 -0
- package/src/api/routes/uploadFileFromBetterRichTextInput.ts +41 -0
- package/src/api/uploadFile.ts +249 -0
- package/src/api/values.ts +96 -0
- package/src/api.ts +94 -1578
- package/src/front/ckeditor5-build/node_modules/semver/bin/semver.js +0 -0
- package/src/index.ts +42 -25
- package/src/types/config.ts +17 -7
- package/src/types/database.ts +2 -0
- package/src/utils/dbManager.ts +99 -14
- package/tests/api-helpers.test.ts +59 -0
- package/tests/dbManager.test.ts +16 -1
- package/tests/helpers/fakeDb.ts +9 -7
- package/tests/integration/authorizationBoundary.integration.test.ts +57 -0
- package/tests/integration/configEnrichment.integration.test.ts +79 -0
- package/tests/integration/customComponentBoundary.integration.test.ts +58 -0
- package/tests/integration/dbManager.integration.test.ts +105 -3
- package/tests/integration/importParserChild.cjs +55 -0
- package/tests/integration/indexRuntime.integration.test.ts +22 -0
- package/tests/integration/indexRuntimeChild.cjs +110 -0
- package/tests/integration/queries.integration.test.ts +88 -0
- package/tests/integration/requestConfig.integration.test.ts +118 -0
- package/tests/integration/routes.authorization.integration.test.ts +1 -0
- package/tests/integration/routes.config.integration.test.ts +39 -1
- package/tests/integration/routes.crud.integration.test.ts +233 -1
- package/tests/integration/routes.custom.integration.test.ts +119 -0
- package/tests/integration/routes.foreign.integration.test.ts +3 -2
- package/tests/integration/routes.importExport.integration.test.ts +308 -6
- package/tests/integration/routes.integration.test.ts +4 -0
- package/tests/integration/routes.records.integration.test.ts +82 -6
- package/tests/integration/routes.upload.integration.test.ts +178 -0
- package/tests/integration/userFilters.integration.test.ts +225 -0
- package/tsconfig.json +1 -1
- package/tsconfig.strict.json +45 -0
- package/dist/front/ckeditor5-build/src/ckeditor.d.ts +0 -43
- package/dist/front/ckeditor5-build/src/ckeditor.js +0 -130
- package/dist/front/ckeditor5-build/src/ckeditor.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/front/ckeditor5-build/node_modules/.bin/semver +0 -12
- package/src/front/ckeditor5-build/node_modules/.bin/semver.cmd +0 -17
- package/src/front/ckeditor5-build/node_modules/.bin/semver.ps1 +0 -28
|
@@ -3,6 +3,7 @@ import * as os from "os";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as request from "supertest";
|
|
5
5
|
import { generateUniqueIdForObject } from "../../src/api";
|
|
6
|
+
import { uploadFile } from "../../src/api/uploadFile";
|
|
6
7
|
import { buildAppForManager } from "../helpers/app";
|
|
7
8
|
import { baseConfig, fruitView } from "../helpers/fixtures";
|
|
8
9
|
import {
|
|
@@ -119,7 +120,49 @@ describeIntegration("upload routes against real databases", () => {
|
|
|
119
120
|
contentType: "image/png",
|
|
120
121
|
});
|
|
121
122
|
|
|
123
|
+
it.each([
|
|
124
|
+
{ s3Config: { host: "s3.example.com", accessKeyId: "k", secretAccessKey: "s", bucket: "b" } },
|
|
125
|
+
{ gcpConfig: { host: "storage.googleapis.com", bucket: "b" } },
|
|
126
|
+
{ publicPath: "/srv/public", sftpConfig: { host: "h", user: "u", password: "p" } },
|
|
127
|
+
{ publicPath: os.tmpdir(), subdirectory: "" },
|
|
128
|
+
])("rejects a missing file for storage config %j", async (storage) => {
|
|
129
|
+
const { app, colId } = appWithColumn(fileColumn(storage));
|
|
130
|
+
const res = await request(app)
|
|
131
|
+
.post("/api/uploadFile")
|
|
132
|
+
.query({ viewIndex: 0, columnId: colId })
|
|
133
|
+
.field("description", "no file")
|
|
134
|
+
.expect(400);
|
|
135
|
+
expect(res.text).toBe("Missing upload file");
|
|
136
|
+
expect(jest.requireMock("@aws-sdk/client-s3").S3Client).not.toHaveBeenCalled();
|
|
137
|
+
expect(jest.requireMock("@google-cloud/storage").Storage).not.toHaveBeenCalled();
|
|
138
|
+
expect(jest.requireMock("ssh2-sftp-client").__sftp.connect).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
122
141
|
describe("S3 backend", () => {
|
|
142
|
+
it.each(["view", "panel"])("finds %s upload columns and prefers view columns for duplicate IDs", async (location) => {
|
|
143
|
+
const sharedId = "upload-target";
|
|
144
|
+
const storage = (bucket: string) => ({
|
|
145
|
+
host: "s3.example.com", accessKeyId: "k", secretAccessKey: "s", bucket,
|
|
146
|
+
});
|
|
147
|
+
const mainColumn = fileColumn({
|
|
148
|
+
__id__: location === "view" ? sharedId : "different-view-column",
|
|
149
|
+
s3Config: storage("view-bucket"),
|
|
150
|
+
});
|
|
151
|
+
const panelColumn = fileColumn({ __id__: sharedId, s3Config: storage("panel-bucket") });
|
|
152
|
+
const instance = buildAppForManager(harness.manager, baseConfig({ views: [fruitView({
|
|
153
|
+
primaryId: "id", columns: [mainColumn],
|
|
154
|
+
recordViewPanels: [
|
|
155
|
+
{ type: "table", label: "No creation", query: "SELECT * FROM fruits" },
|
|
156
|
+
{ type: "table", label: "Creation", query: "SELECT * FROM fruits",
|
|
157
|
+
createConfig: { label: "Create", recordIdKey: "id", columns: [panelColumn] } },
|
|
158
|
+
],
|
|
159
|
+
})] }));
|
|
160
|
+
await s3Request(instance, sharedId).expect(200);
|
|
161
|
+
expect(jest.requireMock("@aws-sdk/client-s3").PutObjectCommand).toHaveBeenCalledWith(
|
|
162
|
+
expect.objectContaining({ Bucket: `${location}-bucket` }),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
123
166
|
it("uploads with a public-read PutObjectCommand and returns the public URL", async () => {
|
|
124
167
|
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
125
168
|
const column = fileColumn({
|
|
@@ -162,6 +205,83 @@ describeIntegration("upload routes against real databases", () => {
|
|
|
162
205
|
expect(Buffer.isBuffer(sentCommand.Body)).toBe(true);
|
|
163
206
|
});
|
|
164
207
|
|
|
208
|
+
it("lets the SDK resolve credentials when no accessKeyId is configured", async () => {
|
|
209
|
+
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
210
|
+
const column = fileColumn({
|
|
211
|
+
s3Config: {
|
|
212
|
+
host: "s3.example.com",
|
|
213
|
+
bucket: "b",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const { app, colId } = appWithColumn(column);
|
|
217
|
+
await s3Request(app, colId).expect(200);
|
|
218
|
+
expect(s3Mock.S3Client.mock.calls[0][0]).not.toHaveProperty(
|
|
219
|
+
"credentials",
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("lets the SDK resolve credentials when accessKeyId is an empty string", async () => {
|
|
224
|
+
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
225
|
+
const column = fileColumn({
|
|
226
|
+
s3Config: {
|
|
227
|
+
host: "s3.example.com",
|
|
228
|
+
accessKeyId: "",
|
|
229
|
+
secretAccessKey: "",
|
|
230
|
+
bucket: "b",
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
const { app, colId } = appWithColumn(column);
|
|
234
|
+
await s3Request(app, colId).expect(200);
|
|
235
|
+
expect(s3Mock.S3Client.mock.calls[0][0]).not.toHaveProperty(
|
|
236
|
+
"credentials",
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("lets the SDK resolve credentials in the signed-URL flow when no accessKeyId is configured", async () => {
|
|
241
|
+
const presignerMock = jest.requireMock("@aws-sdk/s3-request-presigner");
|
|
242
|
+
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
243
|
+
const column = fileColumn({
|
|
244
|
+
s3Config: {
|
|
245
|
+
host: "s3.example.com",
|
|
246
|
+
bucket: "b",
|
|
247
|
+
signedUrlNeeded: true,
|
|
248
|
+
region: "eu-west-3",
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const { app, colId } = appWithColumn(column);
|
|
252
|
+
await s3Request(app, colId).expect(200);
|
|
253
|
+
expect(s3Mock.S3Client.mock.calls[0][0]).not.toHaveProperty(
|
|
254
|
+
"credentials",
|
|
255
|
+
);
|
|
256
|
+
expect(presignerMock.getSignedUrl).toHaveBeenCalledWith(
|
|
257
|
+
s3Mock.S3Client.mock.results[0].value,
|
|
258
|
+
expect.anything(),
|
|
259
|
+
{ expiresIn: 150 },
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it.each([
|
|
264
|
+
["document", Buffer.from("text"), /^document-\d+\.undefined$/, "application/octet-stream"],
|
|
265
|
+
["broken.png", Buffer.from("not an image"), /^broken-\d+\.png$/, "image/png"],
|
|
266
|
+
["empty.txt", Buffer.alloc(0), /^empty-\d+\.txt$/, "text/plain"],
|
|
267
|
+
["photo.PNG", PNG_BUFFER, /^photo-\d+\.PNG$/, "image/png"],
|
|
268
|
+
])("preserves filename and image handling for %s", async (filename, content, expectedPath, contentType) => {
|
|
269
|
+
const { app, colId } = appWithColumn(fileColumn({
|
|
270
|
+
s3Config: { host: "s3.example.com", accessKeyId: "k", secretAccessKey: "s", bucket: "b" },
|
|
271
|
+
}));
|
|
272
|
+
const res = await request(app)
|
|
273
|
+
.post("/api/uploadFile")
|
|
274
|
+
.query({ viewIndex: 0, columnId: colId })
|
|
275
|
+
.attach("file", content, { filename })
|
|
276
|
+
.expect(200);
|
|
277
|
+
expect(res.body.originalFilename).toBe(filename);
|
|
278
|
+
expect(res.body.path).toMatch(expectedPath);
|
|
279
|
+
expect(res.body.aspectRatio).toBeNull();
|
|
280
|
+
expect(jest.requireMock("@aws-sdk/client-s3").PutObjectCommand).toHaveBeenCalledWith(
|
|
281
|
+
expect.objectContaining({ ContentType: contentType, Body: content }),
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
165
285
|
it("prefixes https:// to hosts missing a scheme", async () => {
|
|
166
286
|
const s3Mock = jest.requireMock("@aws-sdk/client-s3");
|
|
167
287
|
const column = fileColumn({
|
|
@@ -303,6 +423,18 @@ describeIntegration("upload routes against real databases", () => {
|
|
|
303
423
|
expect(res.body.aspectRatio).toBe(1); // PNG_BUFFER is a real 1x1 PNG header
|
|
304
424
|
});
|
|
305
425
|
|
|
426
|
+
it("prefers GCP when all remote storage settings are present", async () => {
|
|
427
|
+
const { app, colId } = appWithColumn(fileColumn({
|
|
428
|
+
s3Config: { host: "s3.example.com", accessKeyId: "k", secretAccessKey: "s", bucket: "s3" },
|
|
429
|
+
gcpConfig: { host: "storage.googleapis.com", bucket: "gcp" },
|
|
430
|
+
sftpConfig: { host: "sftp.example.com", user: "u", password: "p" },
|
|
431
|
+
}));
|
|
432
|
+
await s3Request(app, colId).expect(200);
|
|
433
|
+
expect(jest.requireMock("@google-cloud/storage").__bucket).toHaveBeenCalledWith("gcp");
|
|
434
|
+
expect(jest.requireMock("@aws-sdk/client-s3").S3Client).not.toHaveBeenCalled();
|
|
435
|
+
expect(jest.requireMock("ssh2-sftp-client").__sftp.connect).not.toHaveBeenCalled();
|
|
436
|
+
});
|
|
437
|
+
|
|
306
438
|
it("returns 500 when the upload fails", async () => {
|
|
307
439
|
const gcpMock = jest.requireMock("@google-cloud/storage");
|
|
308
440
|
gcpMock.__upload.mockRejectedValueOnce(new Error("gcp down"));
|
|
@@ -324,6 +456,30 @@ describeIntegration("upload routes against real databases", () => {
|
|
|
324
456
|
expect(res.text).toContain("publicPath");
|
|
325
457
|
});
|
|
326
458
|
|
|
459
|
+
it("reads the current SFTP config once per credential inside the callback", async () => {
|
|
460
|
+
const column = fileColumn({ publicPath: "/srv/public" });
|
|
461
|
+
const { app } = appWithColumn(column);
|
|
462
|
+
let reads = 0;
|
|
463
|
+
Object.defineProperty(column, "sftpConfig", {
|
|
464
|
+
get: () => {
|
|
465
|
+
reads += 1;
|
|
466
|
+
return { host: `host-${reads}`, user: `user-${reads}`, password: `password-${reads}` };
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
app.post("/upload-read-order", (req, res) => {
|
|
470
|
+
uploadFile(column, "file", req, res);
|
|
471
|
+
reads = 0;
|
|
472
|
+
});
|
|
473
|
+
await request(app)
|
|
474
|
+
.post("/upload-read-order")
|
|
475
|
+
.attach("file", PNG_BUFFER, { filename: "photo.png" })
|
|
476
|
+
.expect(200);
|
|
477
|
+
expect(reads).toBe(3);
|
|
478
|
+
expect(jest.requireMock("ssh2-sftp-client").__sftp.connect).toHaveBeenCalledWith({
|
|
479
|
+
host: "host-1", port: 22, username: "user-2", password: "password-3",
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
327
483
|
it("fastPuts the temp file to the remote path and cleans it up", async () => {
|
|
328
484
|
const sftpMock = jest.requireMock("ssh2-sftp-client");
|
|
329
485
|
const column = fileColumn({
|
|
@@ -380,6 +536,18 @@ describeIntegration("upload routes against real databases", () => {
|
|
|
380
536
|
expect(fs.existsSync(stored)).toBe(true);
|
|
381
537
|
});
|
|
382
538
|
|
|
539
|
+
it("uses the default temporary directory when both path properties are absent", async () => {
|
|
540
|
+
const { app, colId } = appWithColumn(fileColumn());
|
|
541
|
+
const res = await s3Request(app, colId).expect(200);
|
|
542
|
+
try {
|
|
543
|
+
expect(path.dirname(res.body.path)).toBe(os.tmpdir());
|
|
544
|
+
expect(res.body.originalFilename).toBe("photo.png");
|
|
545
|
+
expect(fs.readFileSync(res.body.path)).toEqual(PNG_BUFFER);
|
|
546
|
+
} finally {
|
|
547
|
+
fs.rmSync(res.body.path, { force: true });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
383
551
|
it("fails without a subdirectory (publicPath + undefined concat — documented quirk)", async () => {
|
|
384
552
|
// uploadDir becomes "<publicPath>undefined", which never exists, so
|
|
385
553
|
// the multipart parse fails with 500. A subdirectory is effectively
|
|
@@ -500,6 +668,16 @@ describeIntegration("upload routes against real databases", () => {
|
|
|
500
668
|
expectNoStorage();
|
|
501
669
|
});
|
|
502
670
|
|
|
671
|
+
it("rejects a role-restricted panel column when the session has no user", async () => {
|
|
672
|
+
const column = richColumn({ roles: ["editor"] });
|
|
673
|
+
const app = buildAppForManager(harness.manager, baseConfig({ views: [fruitView({
|
|
674
|
+
recordViewPanels: [{ type: "table", label: "Notes", query: "SELECT * FROM fruit_notes",
|
|
675
|
+
createConfig: { tableName: "fruit_notes", recordIdKey: "fruitId", columns: [column] } }],
|
|
676
|
+
})] }), null);
|
|
677
|
+
await upload(app, { viewIndex: 0, columnId: generateUniqueIdForObject(column) }).expect(403);
|
|
678
|
+
expectNoStorage();
|
|
679
|
+
});
|
|
680
|
+
|
|
503
681
|
it("uses indexes from the role-filtered views", async () => {
|
|
504
682
|
const column = richColumn();
|
|
505
683
|
const app = buildAppForManager(harness.manager, baseConfig({ views: [
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as express from "express";
|
|
2
|
+
import * as request from "supertest";
|
|
3
|
+
import type { RequestHandler } from "express";
|
|
4
|
+
import createRecordQueries from "../../src/api/queries";
|
|
5
|
+
import recordsRoute from "../../src/api/routes/records";
|
|
6
|
+
import exportRoute from "../../src/api/routes/recordsExport";
|
|
7
|
+
import foreignRoute from "../../src/api/routes/foreignSelectRecords";
|
|
8
|
+
import { baseConfig, fruitView } from "../helpers/fixtures";
|
|
9
|
+
import { buildAppForManager } from "../helpers/app";
|
|
10
|
+
import { DbHarness, describeIntegration, getMysqlHarness, getMysql2Harness, getPgHarness, resetTables } from "./dbHarness";
|
|
11
|
+
|
|
12
|
+
type CallbackUser = { role: string; userId?: string | number };
|
|
13
|
+
type Filter = (user: CallbackUser) => string;
|
|
14
|
+
type RouteName = "records" | "recordsExport" | "foreignSelectRecords";
|
|
15
|
+
|
|
16
|
+
// The forwarding recorder below runs the real query helper and database. It
|
|
17
|
+
// observes whether a data query was invoked; it does not replace query results.
|
|
18
|
+
describeIntegration("configured user filters against real databases", () => {
|
|
19
|
+
let harnesses: DbHarness[];
|
|
20
|
+
beforeAll(async () => { harnesses = [await getMysqlHarness(), await getMysql2Harness(), await getPgHarness()]; });
|
|
21
|
+
afterAll(async () => { for (const harness of harnesses || []) await harness.stop(); });
|
|
22
|
+
|
|
23
|
+
describe.each([0, 1, 2])("backend %s", (backend) => {
|
|
24
|
+
let harness: DbHarness;
|
|
25
|
+
beforeEach(async () => { harness = harnesses[backend]; await resetTables(harness); });
|
|
26
|
+
|
|
27
|
+
const literal = (value: string | number) => "'" + (harness.dbType === "POSTGRESQL"
|
|
28
|
+
? String(value).replace(/'/g, "''")
|
|
29
|
+
: String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'")) + "'";
|
|
30
|
+
const scoped = (user: CallbackUser) => user.role === "root" ? ""
|
|
31
|
+
: user.userId === undefined ? "1 = 0" : `slug = ${literal(user.userId)}`;
|
|
32
|
+
|
|
33
|
+
const seed = async (id: string | number) => {
|
|
34
|
+
for (const row of [
|
|
35
|
+
["keep mine", String(id), 1], ["keep other", "someone-else", 1],
|
|
36
|
+
["blocked mine", String(id), 1], ["keep inactive", String(id), 0],
|
|
37
|
+
]) {
|
|
38
|
+
const { err } = await harness.query("INSERT INTO fruits (fruitName, slug, acidity) VALUES (?, ?, ?)", row);
|
|
39
|
+
expect(err).toBeNull();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const build = (route: RouteName, user: unknown, filter?: Filter, events: string[] = []) => {
|
|
44
|
+
const column = {
|
|
45
|
+
__id__: "foreign", label: "Name", dataType: "foreign", name: "fruitName",
|
|
46
|
+
table: "fruits", foreignName: "id", searchableName: "fruitName", filters: "acidity > 0",
|
|
47
|
+
};
|
|
48
|
+
const view = fruitView({
|
|
49
|
+
tableName: "fruits", primaryId: "id", filters: "acidity > 0", columns: [column],
|
|
50
|
+
});
|
|
51
|
+
const target = route === "foreignSelectRecords" ? column : view;
|
|
52
|
+
if (filter) Object.defineProperty(target, "filtersFromUser", { value: filter, configurable: true });
|
|
53
|
+
const config = baseConfig({ enriched: true, views: [view] });
|
|
54
|
+
const session = { user };
|
|
55
|
+
const actual = createRecordQueries(harness.manager).getRecords;
|
|
56
|
+
let queries = 0;
|
|
57
|
+
const getRecords: typeof actual = async (...args) => {
|
|
58
|
+
queries++;
|
|
59
|
+
events.push("query");
|
|
60
|
+
return actual(...args);
|
|
61
|
+
};
|
|
62
|
+
const handler = route === "records" ? recordsRoute(getRecords)
|
|
63
|
+
: route === "recordsExport" ? exportRoute(getRecords)
|
|
64
|
+
: foreignRoute(harness.manager, getRecords);
|
|
65
|
+
const errors: unknown[] = [];
|
|
66
|
+
const app = express();
|
|
67
|
+
app.use(express.json());
|
|
68
|
+
app.use((req, _res, next) => {
|
|
69
|
+
req.config = config;
|
|
70
|
+
Object.defineProperty(req, "session", { value: session });
|
|
71
|
+
next();
|
|
72
|
+
});
|
|
73
|
+
const observe: RequestHandler = async (req, res, next) => {
|
|
74
|
+
try { await handler(req, res, next); }
|
|
75
|
+
catch (error) { errors.push(error); res.sendStatus(599); }
|
|
76
|
+
};
|
|
77
|
+
app.post("/filter", observe);
|
|
78
|
+
const send = () => request(app).post("/filter").send({
|
|
79
|
+
viewIndex: 0, columnId: "foreign", page: 1,
|
|
80
|
+
filters: route === "records" ? ["fruitName LIKE 'keep%'"] : "fruitName LIKE 'keep%'",
|
|
81
|
+
...(route === "foreignSelectRecords" ? { query: "keep" } : {}),
|
|
82
|
+
});
|
|
83
|
+
return { target, session, errors, send, queries: () => queries };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const names = (route: RouteName, body: { records?: { fruitName: string }[]; content?: string } | { label: string }[]) => {
|
|
87
|
+
if (Array.isArray(body)) return body.map((row) => row.label);
|
|
88
|
+
return route === "records" ? body.records?.map((row) => row.fruitName) : body.content?.split("\r\n").slice(1);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe.each<RouteName>(["records", "recordsExport", "foreignSelectRecords"])("route %s", (route) => {
|
|
92
|
+
it.each([
|
|
93
|
+
["number", 2], ["zero", 0], ["numeric string", "002"],
|
|
94
|
+
["UUID", "00000000-0000-4000-8000-000000000001"], ["SQL punctuation", "owner' OR '1'='1"],
|
|
95
|
+
])("FILTER USER POLICY: scopes an exact %s ID and keeps the session", async (_label, id) => {
|
|
96
|
+
await seed(id);
|
|
97
|
+
const user = Object.freeze({ role: "editor", userId: id, extra: "stored-only" });
|
|
98
|
+
const received: unknown[] = [];
|
|
99
|
+
const instance = build(route, user, function (input) {
|
|
100
|
+
expect(this).toBe(instance.target);
|
|
101
|
+
expect(arguments).toHaveLength(1);
|
|
102
|
+
received.push(input);
|
|
103
|
+
return scoped(input);
|
|
104
|
+
});
|
|
105
|
+
const response = await instance.send();
|
|
106
|
+
expect(response.status).toBe(200);
|
|
107
|
+
expect(received).toEqual([{ role: "editor", userId: id }]);
|
|
108
|
+
expect(received[0]).not.toBe(user);
|
|
109
|
+
expect(instance.session.user).toBe(user);
|
|
110
|
+
expect(names(route, response.body)).toEqual(["keep mine"]);
|
|
111
|
+
expect(instance.queries()).toBe(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("FILTER USER POLICY: invokes root filters without reading or forwarding an ID", async () => {
|
|
115
|
+
await seed(2);
|
|
116
|
+
let idReads = 0;
|
|
117
|
+
const user = { role: "root", get userId() { idReads++; throw new Error("root ID read"); } };
|
|
118
|
+
let calls = 0;
|
|
119
|
+
const instance = build(route, user, function (input) {
|
|
120
|
+
expect(this).toBe(instance.target);
|
|
121
|
+
expect(arguments).toHaveLength(1);
|
|
122
|
+
expect(input).toEqual({ role: "root" });
|
|
123
|
+
calls++;
|
|
124
|
+
return "fruitName = 'keep other'";
|
|
125
|
+
});
|
|
126
|
+
const response = await instance.send();
|
|
127
|
+
expect(response.status).toBe(200);
|
|
128
|
+
expect(names(route, response.body)).toEqual(["keep other"]);
|
|
129
|
+
expect({ calls, idReads, queries: instance.queries() }).toEqual({ calls: 1, idReads: 0, queries: 1 });
|
|
130
|
+
expect(instance.session.user).toBe(user);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it.each([{}, { userId: undefined }])("FILTER USER POLICY: lets a regular user without an ID choose a role policy %j", async (idFields) => {
|
|
134
|
+
await seed(2);
|
|
135
|
+
const user = Object.freeze({ role: "editor", ...idFields });
|
|
136
|
+
let calls = 0;
|
|
137
|
+
const instance = build(route, user, (input) => {
|
|
138
|
+
expect(input).toEqual({ role: "editor" });
|
|
139
|
+
expect(Object.prototype.hasOwnProperty.call(input, "userId")).toBe(false);
|
|
140
|
+
calls++;
|
|
141
|
+
return "fruitName = 'keep mine'";
|
|
142
|
+
});
|
|
143
|
+
const response = await instance.send();
|
|
144
|
+
expect(response.status).toBe(200);
|
|
145
|
+
expect(names(route, response.body)).toEqual(["keep mine"]);
|
|
146
|
+
expect(calls).toBe(1);
|
|
147
|
+
expect(instance.session.user).toBe(user);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("FILTER USER POLICY: accepts unnormalized numeric values and empty identifiers", async () => {
|
|
151
|
+
for (const id of [-2.5, NaN, Infinity, ""]) {
|
|
152
|
+
let received: unknown;
|
|
153
|
+
const instance = build(route, { role: "", userId: id }, (input) => {
|
|
154
|
+
received = input;
|
|
155
|
+
return "1 = 0";
|
|
156
|
+
});
|
|
157
|
+
await instance.send().expect(200);
|
|
158
|
+
expect(received).toEqual({ role: "", userId: id });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("FILTER USER POLICY: rejects malformed sessions before callback or data query", async () => {
|
|
163
|
+
const invalid: unknown[] = [undefined, null, [], "editor", 7, false, () => {}, {},
|
|
164
|
+
{ role: null }, { role: 1 }, { role: [] },
|
|
165
|
+
...[null, true, [], {}, BigInt(1), Symbol("id"), () => {}].map((userId) => ({ role: "editor", userId }))];
|
|
166
|
+
for (const user of invalid) {
|
|
167
|
+
let calls = 0;
|
|
168
|
+
const instance = build(route, user, () => { calls++; return ""; });
|
|
169
|
+
const response = await instance.send();
|
|
170
|
+
expect({ status: response.status, calls, queries: instance.queries(), errors: instance.errors }).toEqual({
|
|
171
|
+
status: 401, calls: 0, queries: 0, errors: [],
|
|
172
|
+
});
|
|
173
|
+
expect(instance.session.user).toBe(user);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("does not read or validate the session when no user filter is configured", async () => {
|
|
178
|
+
await seed(2);
|
|
179
|
+
const instance = build(route, null);
|
|
180
|
+
Object.defineProperty(instance.session, "user", { get() { throw new Error("unexpected session read"); } });
|
|
181
|
+
const response = await instance.send().expect(200);
|
|
182
|
+
expect(names(route, response.body)).toEqual(["keep mine", "keep other"]);
|
|
183
|
+
expect(instance.queries()).toBe(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("preserves callback receiver, count, thrown error and no-query behavior", async () => {
|
|
187
|
+
const failure = new Error("filter failure");
|
|
188
|
+
let calls = 0;
|
|
189
|
+
const instance = build(route, { role: "editor", userId: 2 }, function () {
|
|
190
|
+
expect(this).toBe(instance.target);
|
|
191
|
+
calls++;
|
|
192
|
+
throw failure;
|
|
193
|
+
});
|
|
194
|
+
await instance.send().expect(599);
|
|
195
|
+
expect(instance.errors).toEqual([failure]);
|
|
196
|
+
expect(instance.errors[0]).toBe(failure);
|
|
197
|
+
expect(calls).toBe(1);
|
|
198
|
+
expect(instance.queries()).toBe(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("preserves table/filter/callback property evaluation order", async () => {
|
|
202
|
+
const events: string[] = [];
|
|
203
|
+
const filter = function () { events.push("callback"); return "1 = 0"; };
|
|
204
|
+
const instance = build(route, { role: "editor", userId: 2 }, filter, events);
|
|
205
|
+
Object.defineProperty(instance.target, route === "foreignSelectRecords" ? "table" : "tableName", {
|
|
206
|
+
get() { events.push("table"); return "fruits"; },
|
|
207
|
+
});
|
|
208
|
+
Object.defineProperty(instance.target, "filters", { get() { events.push("filters"); return "acidity > 0"; } });
|
|
209
|
+
Object.defineProperty(instance.target, "filtersFromUser", { get() { events.push("filter property"); return filter; } });
|
|
210
|
+
await instance.send().expect(200);
|
|
211
|
+
expect(events).toEqual(["table", "filters", "filter property", "filter property", "callback", "query"]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("keeps API request-filter authorization before a configured user filter", async () => {
|
|
216
|
+
let calls = 0;
|
|
217
|
+
const app = buildAppForManager(harness.manager, baseConfig({ views: [fruitView({
|
|
218
|
+
filtersFromUser: () => { calls++; return ""; },
|
|
219
|
+
})] }), null);
|
|
220
|
+
await request(app).post("/api/records").send({ viewIndex: 0, page: 1, filters: ["1=1; DROP TABLE fruits"] }).expect(400);
|
|
221
|
+
expect(calls).toBe(0);
|
|
222
|
+
expect((await harness.query("SELECT * FROM fruits")).rows).toEqual([]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"strict": true
|
|
5
|
+
},
|
|
6
|
+
"include": [
|
|
7
|
+
"src/types/**/*.ts",
|
|
8
|
+
"src/utils/imageDimensions.ts",
|
|
9
|
+
"src/utils/dbManager.ts",
|
|
10
|
+
"src/api/config.ts",
|
|
11
|
+
"src/api/queries.ts",
|
|
12
|
+
"src/api/authorization.ts",
|
|
13
|
+
"src/api/uploadFile.ts",
|
|
14
|
+
"src/api/request.ts",
|
|
15
|
+
"src/api/routes/adjacentRecordId.ts",
|
|
16
|
+
"src/api/routes/config.ts",
|
|
17
|
+
"src/api/routes/customAction.ts",
|
|
18
|
+
"src/api/routes/customComponent.ts",
|
|
19
|
+
"src/api/routes/customQuery.ts",
|
|
20
|
+
"src/api/routes/deleteRecords.ts",
|
|
21
|
+
"src/api/routes/duplicateRecord.ts",
|
|
22
|
+
"src/api/routes/notificationBadges.ts",
|
|
23
|
+
"src/api/routes/record.ts",
|
|
24
|
+
"src/api/routes/searchCompanies.ts",
|
|
25
|
+
"src/api/routes/uploadFile.ts",
|
|
26
|
+
"src/api/routes/createRecord.ts",
|
|
27
|
+
"src/api/routes/deleteRecord.ts",
|
|
28
|
+
"src/api/routes/deleteRecordFromPanel.ts",
|
|
29
|
+
"src/api/routes/updateRecord.ts",
|
|
30
|
+
"src/api/values.ts",
|
|
31
|
+
"src/api/importRecords.ts",
|
|
32
|
+
"src/api/routes/recordsImport.ts",
|
|
33
|
+
"src/api/routes/subrecordsImport.ts",
|
|
34
|
+
"src/api/routes/uploadFileFromBetterRichTextInput.ts",
|
|
35
|
+
"src/api/filterUser.ts",
|
|
36
|
+
"src/api/routes/records.ts",
|
|
37
|
+
"src/api/routes/recordsExport.ts",
|
|
38
|
+
"src/api/routes/foreignSelectRecords.ts",
|
|
39
|
+
"src/api.ts",
|
|
40
|
+
"src/index.ts"
|
|
41
|
+
],
|
|
42
|
+
"exclude": [
|
|
43
|
+
"src/front"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
|
|
2
|
-
import { Alignment } from '@ckeditor/ckeditor5-alignment';
|
|
3
|
-
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
|
|
4
|
-
import { Bold, Italic, Strikethrough, Underline } from '@ckeditor/ckeditor5-basic-styles';
|
|
5
|
-
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
|
|
6
|
-
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services';
|
|
7
|
-
import { Essentials } from '@ckeditor/ckeditor5-essentials';
|
|
8
|
-
import { FindAndReplace } from '@ckeditor/ckeditor5-find-and-replace';
|
|
9
|
-
import { FontBackgroundColor, FontColor } from '@ckeditor/ckeditor5-font';
|
|
10
|
-
import { Heading } from '@ckeditor/ckeditor5-heading';
|
|
11
|
-
import { Highlight } from '@ckeditor/ckeditor5-highlight';
|
|
12
|
-
import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line';
|
|
13
|
-
import { HtmlEmbed } from '@ckeditor/ckeditor5-html-embed';
|
|
14
|
-
import { AutoImage, Image, ImageCaption, ImageInsert, ImageResize, ImageStyle, ImageToolbar, ImageUpload } from '@ckeditor/ckeditor5-image';
|
|
15
|
-
import { Indent, IndentBlock } from '@ckeditor/ckeditor5-indent';
|
|
16
|
-
import { AutoLink, Link, LinkImage } from '@ckeditor/ckeditor5-link';
|
|
17
|
-
import { List } from '@ckeditor/ckeditor5-list';
|
|
18
|
-
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed';
|
|
19
|
-
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
|
|
20
|
-
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
|
|
21
|
-
import { RemoveFormat } from '@ckeditor/ckeditor5-remove-format';
|
|
22
|
-
import { SelectAll } from '@ckeditor/ckeditor5-select-all';
|
|
23
|
-
import { SourceEditing } from '@ckeditor/ckeditor5-source-editing';
|
|
24
|
-
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table';
|
|
25
|
-
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
|
|
26
|
-
import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload';
|
|
27
|
-
import { WordCount } from '@ckeditor/ckeditor5-word-count';
|
|
28
|
-
declare class Editor extends ClassicEditor {
|
|
29
|
-
static builtinPlugins: (typeof Alignment | typeof AutoImage | typeof AutoLink | typeof Autoformat | typeof BlockQuote | typeof Bold | typeof CloudServices | typeof Essentials | typeof FindAndReplace | typeof FontBackgroundColor | typeof FontColor | typeof Heading | typeof Highlight | typeof HorizontalLine | typeof HtmlEmbed | typeof Image | typeof ImageCaption | typeof ImageInsert | typeof ImageResize | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof Indent | typeof IndentBlock | typeof Italic | typeof Link | typeof LinkImage | typeof List | typeof MediaEmbed | typeof Paragraph | typeof PasteFromOffice | typeof RemoveFormat | typeof SelectAll | typeof SimpleUploadAdapter | typeof SourceEditing | typeof Strikethrough | typeof Table | typeof TableToolbar | typeof TextTransformation | typeof Underline | typeof WordCount)[];
|
|
30
|
-
static defaultConfig: {
|
|
31
|
-
toolbar: {
|
|
32
|
-
items: string[];
|
|
33
|
-
};
|
|
34
|
-
language: string;
|
|
35
|
-
image: {
|
|
36
|
-
toolbar: string[];
|
|
37
|
-
};
|
|
38
|
-
table: {
|
|
39
|
-
contentToolbar: string[];
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
export default Editor;
|