datocms-plugin-record-bin 2.0.0 → 3.0.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/README.md +127 -11
- package/build/assets/index-BnrW9Ts8.js +15 -0
- package/build/assets/index-aWCW2c0n.css +1 -0
- package/build/index.html +13 -1
- package/index.html +12 -0
- package/package.json +24 -18
- package/src/entrypoints/BinOutlet.tsx +262 -37
- package/src/entrypoints/ConfigScreen.tsx +939 -38
- package/src/entrypoints/ErrorModal.tsx +86 -2
- package/src/index.tsx +73 -28
- package/src/react-app-env.d.ts +1 -1
- package/src/types/types.ts +36 -8
- package/src/utils/binCleanup.test.ts +107 -0
- package/src/utils/binCleanup.ts +71 -23
- package/src/utils/debugLogger.ts +27 -0
- package/src/utils/deployProviders.test.ts +33 -0
- package/src/utils/deployProviders.ts +28 -0
- package/src/utils/getDeploymentUrlFromParameters.test.ts +26 -0
- package/src/utils/getDeploymentUrlFromParameters.ts +21 -0
- package/src/utils/getRuntimeMode.test.ts +57 -0
- package/src/utils/getRuntimeMode.ts +23 -0
- package/src/utils/lambdaLessCapture.test.ts +218 -0
- package/src/utils/lambdaLessCapture.ts +160 -0
- package/src/utils/lambdaLessCleanup.test.ts +125 -0
- package/src/utils/lambdaLessCleanup.ts +69 -0
- package/src/utils/lambdaLessRestore.test.ts +248 -0
- package/src/utils/lambdaLessRestore.ts +159 -0
- package/src/utils/recordBinModel.ts +108 -0
- package/src/utils/recordBinPayload.test.ts +103 -0
- package/src/utils/recordBinPayload.ts +136 -0
- package/src/utils/recordBinWebhook.test.ts +253 -0
- package/src/utils/recordBinWebhook.ts +305 -0
- package/src/utils/render.tsx +17 -8
- package/src/utils/restoreError.test.ts +112 -0
- package/src/utils/restoreError.ts +221 -0
- package/src/utils/verifyLambdaHealth.test.ts +248 -0
- package/src/utils/verifyLambdaHealth.ts +422 -0
- package/vite.config.ts +11 -0
- package/build/asset-manifest.json +0 -13
- package/build/static/css/main.10f29737.css +0 -2
- package/build/static/css/main.10f29737.css.map +0 -1
- package/build/static/js/main.53795e3b.js +0 -3
- package/build/static/js/main.53795e3b.js.LICENSE.txt +0 -47
- package/build/static/js/main.53795e3b.js.map +0 -1
- package/src/entrypoints/InstallationModal.tsx +0 -107
- package/src/entrypoints/PreInstallConfig.tsx +0 -28
- package/src/utils/attemptVercelInitialization.ts +0 -16
|
@@ -1,22 +1,106 @@
|
|
|
1
1
|
import { RenderModalCtx } from "datocms-plugin-sdk";
|
|
2
2
|
import { Button, Canvas } from "datocms-react-ui";
|
|
3
|
+
import { createDebugLogger, isDebugEnabled } from "../utils/debugLogger";
|
|
3
4
|
|
|
4
5
|
type PropTypes = {
|
|
5
6
|
ctx: RenderModalCtx;
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
const ErrorModal = ({ ctx }: PropTypes) => {
|
|
10
|
+
const debugLogger = createDebugLogger(
|
|
11
|
+
isDebugEnabled(ctx.plugin.attributes.parameters),
|
|
12
|
+
"ErrorModal"
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const copyTextToClipboard = async (text: string) => {
|
|
16
|
+
if (
|
|
17
|
+
typeof navigator !== "undefined" &&
|
|
18
|
+
navigator.clipboard &&
|
|
19
|
+
typeof navigator.clipboard.writeText === "function"
|
|
20
|
+
) {
|
|
21
|
+
await navigator.clipboard.writeText(text);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof document === "undefined") {
|
|
26
|
+
throw new Error("Clipboard API is unavailable in this environment.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const textArea = document.createElement("textarea");
|
|
30
|
+
textArea.value = text;
|
|
31
|
+
textArea.setAttribute("readonly", "");
|
|
32
|
+
textArea.style.position = "fixed";
|
|
33
|
+
textArea.style.left = "-9999px";
|
|
34
|
+
document.body.appendChild(textArea);
|
|
35
|
+
textArea.select();
|
|
36
|
+
|
|
37
|
+
const didCopy = document.execCommand("copy");
|
|
38
|
+
document.body.removeChild(textArea);
|
|
39
|
+
|
|
40
|
+
if (!didCopy) {
|
|
41
|
+
throw new Error("execCommand copy failed.");
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
9
45
|
const handleCancelationButtonClick = () => {
|
|
46
|
+
debugLogger.log("Closing restoration error modal");
|
|
10
47
|
ctx.resolve("done");
|
|
11
48
|
};
|
|
12
49
|
|
|
13
|
-
const
|
|
50
|
+
const errorPayloadText = (ctx.parameters.errorPayload as string)
|
|
14
51
|
.replaceAll("\\n", "\n")
|
|
15
52
|
.replaceAll("\\", "");
|
|
16
53
|
|
|
54
|
+
const handleCopyButtonClick = async () => {
|
|
55
|
+
try {
|
|
56
|
+
await copyTextToClipboard(errorPayloadText);
|
|
57
|
+
debugLogger.log("Copied restoration error payload to clipboard", {
|
|
58
|
+
payloadLength: errorPayloadText.length,
|
|
59
|
+
});
|
|
60
|
+
await ctx.notice("Restoration error copied to clipboard.");
|
|
61
|
+
} catch (error) {
|
|
62
|
+
debugLogger.warn("Failed to copy restoration error payload", error);
|
|
63
|
+
await ctx.alert("Could not copy restoration error to clipboard.");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
debugLogger.log("Rendering error payload in modal", {
|
|
68
|
+
payloadLength: errorPayloadText.length,
|
|
69
|
+
});
|
|
70
|
+
|
|
17
71
|
return (
|
|
18
72
|
<Canvas ctx={ctx}>
|
|
19
|
-
<
|
|
73
|
+
<Button onClick={handleCopyButtonClick} fullWidth buttonType="muted">
|
|
74
|
+
Copy error to clipboard
|
|
75
|
+
</Button>
|
|
76
|
+
<div
|
|
77
|
+
style={{
|
|
78
|
+
marginTop: "var(--spacing-s)",
|
|
79
|
+
marginBottom: "var(--spacing-s)",
|
|
80
|
+
border: "1px solid var(--border-color)",
|
|
81
|
+
borderRadius: "6px",
|
|
82
|
+
background: "#f8f9fb",
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<pre
|
|
86
|
+
style={{
|
|
87
|
+
margin: 0,
|
|
88
|
+
padding: "var(--spacing-m)",
|
|
89
|
+
maxHeight: "320px",
|
|
90
|
+
overflowY: "auto",
|
|
91
|
+
overflowX: "auto",
|
|
92
|
+
whiteSpace: "pre-wrap",
|
|
93
|
+
overflowWrap: "anywhere",
|
|
94
|
+
wordBreak: "break-word",
|
|
95
|
+
fontSize: "12px",
|
|
96
|
+
lineHeight: "1.5",
|
|
97
|
+
fontFamily:
|
|
98
|
+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<code>{errorPayloadText}</code>
|
|
102
|
+
</pre>
|
|
103
|
+
</div>
|
|
20
104
|
<Button
|
|
21
105
|
onClick={handleCancelationButtonClick}
|
|
22
106
|
fullWidth
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
connect,
|
|
3
|
-
|
|
3
|
+
ItemFormOutletsCtx,
|
|
4
4
|
ItemType,
|
|
5
5
|
RenderItemFormOutletCtx,
|
|
6
6
|
RenderModalCtx,
|
|
@@ -9,37 +9,64 @@ import { render } from "./utils/render";
|
|
|
9
9
|
import ConfigScreen from "./entrypoints/ConfigScreen";
|
|
10
10
|
import "datocms-react-ui/styles.css";
|
|
11
11
|
import BinOutlet from "./entrypoints/BinOutlet";
|
|
12
|
-
import InstallationModal from "./entrypoints/InstallationModal";
|
|
13
|
-
import PreInstallConfig from "./entrypoints/PreInstallConfig";
|
|
14
12
|
import ErrorModal from "./entrypoints/ErrorModal";
|
|
15
13
|
import binCleanup from "./utils/binCleanup";
|
|
14
|
+
import { createDebugLogger, isDebugEnabled } from "./utils/debugLogger";
|
|
15
|
+
import { getRuntimeMode } from "./utils/getRuntimeMode";
|
|
16
|
+
import { captureDeletedItemsWithoutLambda } from "./utils/lambdaLessCapture";
|
|
16
17
|
|
|
17
18
|
connect({
|
|
18
19
|
async onBoot(ctx) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
width: "m",
|
|
28
|
-
parameters: { foo: "bar" },
|
|
29
|
-
closeDisabled: true,
|
|
30
|
-
});
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
20
|
+
const pluginParameters = ctx.plugin.attributes.parameters;
|
|
21
|
+
const debugLogger = createDebugLogger(
|
|
22
|
+
isDebugEnabled(pluginParameters),
|
|
23
|
+
"index.onBoot"
|
|
24
|
+
);
|
|
25
|
+
debugLogger.log("Plugin boot started");
|
|
26
|
+
|
|
27
|
+
debugLogger.log("Running daily cleanup check");
|
|
33
28
|
await binCleanup(ctx);
|
|
29
|
+
debugLogger.log("Plugin boot completed");
|
|
34
30
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
async onBeforeItemsDestroy(items, ctx) {
|
|
32
|
+
const debugLogger = createDebugLogger(
|
|
33
|
+
isDebugEnabled(ctx.plugin.attributes.parameters),
|
|
34
|
+
"index.onBeforeItemsDestroy"
|
|
35
|
+
);
|
|
36
|
+
const runtimeMode = getRuntimeMode(ctx.plugin.attributes.parameters);
|
|
37
|
+
|
|
38
|
+
if (runtimeMode === "lambda") {
|
|
39
|
+
debugLogger.log("Skipping Lambda-less delete capture because lambda mode is active");
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const captureResult = await captureDeletedItemsWithoutLambda(items, ctx);
|
|
45
|
+
debugLogger.log("Lambda-less delete capture completed", captureResult);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
debugLogger.error(
|
|
48
|
+
"Unexpected error in Lambda-less delete capture. Proceeding with deletion (fail-open).",
|
|
49
|
+
error
|
|
50
|
+
);
|
|
38
51
|
}
|
|
39
|
-
|
|
52
|
+
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
renderConfigScreen(ctx) {
|
|
56
|
+
const debugLogger = createDebugLogger(
|
|
57
|
+
isDebugEnabled(ctx.plugin.attributes.parameters),
|
|
58
|
+
"index.renderConfigScreen"
|
|
59
|
+
);
|
|
60
|
+
debugLogger.log("Rendering config screen");
|
|
61
|
+
return render(<ConfigScreen ctx={ctx} />);
|
|
40
62
|
},
|
|
41
|
-
itemFormOutlets(model: ItemType,
|
|
63
|
+
itemFormOutlets(model: ItemType, _ctx: ItemFormOutletsCtx) {
|
|
64
|
+
const debugLogger = createDebugLogger(
|
|
65
|
+
isDebugEnabled(_ctx.plugin.attributes.parameters),
|
|
66
|
+
"index.itemFormOutlets"
|
|
67
|
+
);
|
|
42
68
|
if (model.attributes.api_key === "record_bin") {
|
|
69
|
+
debugLogger.log("Registering item form outlet for record_bin model");
|
|
43
70
|
return [
|
|
44
71
|
{
|
|
45
72
|
id: "recordBin",
|
|
@@ -47,22 +74,40 @@ connect({
|
|
|
47
74
|
},
|
|
48
75
|
];
|
|
49
76
|
}
|
|
77
|
+
|
|
78
|
+
debugLogger.log("Skipping item form outlet for model", {
|
|
79
|
+
modelApiKey: model.attributes.api_key,
|
|
80
|
+
});
|
|
50
81
|
return [];
|
|
51
82
|
},
|
|
52
83
|
renderItemFormOutlet(outletId, ctx: RenderItemFormOutletCtx) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
84
|
+
const debugLogger = createDebugLogger(
|
|
85
|
+
isDebugEnabled(ctx.plugin.attributes.parameters),
|
|
86
|
+
"index.renderItemFormOutlet"
|
|
87
|
+
);
|
|
88
|
+
if (outletId === "recordBin") {
|
|
89
|
+
debugLogger.log("Rendering record bin outlet");
|
|
57
90
|
render(<BinOutlet ctx={ctx} />);
|
|
91
|
+
return;
|
|
58
92
|
}
|
|
93
|
+
|
|
94
|
+
debugLogger.log("Skipping outlet rendering", {
|
|
95
|
+
outletId,
|
|
96
|
+
});
|
|
59
97
|
},
|
|
60
98
|
renderModal(modalId: string, ctx: RenderModalCtx) {
|
|
99
|
+
const debugLogger = createDebugLogger(
|
|
100
|
+
isDebugEnabled(ctx.plugin.attributes.parameters),
|
|
101
|
+
"index.renderModal"
|
|
102
|
+
);
|
|
103
|
+
debugLogger.log("Rendering modal", { modalId });
|
|
104
|
+
|
|
61
105
|
switch (modalId) {
|
|
62
|
-
case "installationModal":
|
|
63
|
-
return render(<InstallationModal ctx={ctx} />);
|
|
64
106
|
case "errorModal":
|
|
65
107
|
return render(<ErrorModal ctx={ctx} />);
|
|
108
|
+
default:
|
|
109
|
+
debugLogger.warn("Received unknown modal id", { modalId });
|
|
110
|
+
return undefined;
|
|
66
111
|
}
|
|
67
112
|
},
|
|
68
113
|
});
|
package/src/react-app-env.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
/// <reference types="
|
|
1
|
+
/// <reference types="vite/client" />
|
package/src/types/types.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
export type errorObject = {
|
|
2
2
|
simplifiedError: {
|
|
3
|
-
code
|
|
3
|
+
code?: string;
|
|
4
4
|
details: {
|
|
5
|
-
code
|
|
6
|
-
field
|
|
7
|
-
field_id
|
|
8
|
-
field_label
|
|
9
|
-
field_type
|
|
10
|
-
extraneous_attributes
|
|
11
|
-
fullPayload
|
|
5
|
+
code?: string;
|
|
6
|
+
field?: string;
|
|
7
|
+
field_id?: string;
|
|
8
|
+
field_label?: string;
|
|
9
|
+
field_type?: string;
|
|
10
|
+
extraneous_attributes?: string[];
|
|
11
|
+
fullPayload?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
12
13
|
};
|
|
14
|
+
[key: string]: unknown;
|
|
13
15
|
};
|
|
14
16
|
fullErrorPayload: string;
|
|
15
17
|
};
|
|
@@ -18,3 +20,29 @@ export type automaticBinCleanupObject = {
|
|
|
18
20
|
numberOfDays: number;
|
|
19
21
|
timeStamp: string;
|
|
20
22
|
};
|
|
23
|
+
|
|
24
|
+
export type LambdaConnectionStatus = "connected" | "disconnected";
|
|
25
|
+
|
|
26
|
+
export type LambdaConnectionPhase =
|
|
27
|
+
| "finish_installation"
|
|
28
|
+
| "config_mount"
|
|
29
|
+
| "config_connect";
|
|
30
|
+
|
|
31
|
+
export type LambdaConnectionErrorCode =
|
|
32
|
+
| "INVALID_URL"
|
|
33
|
+
| "NETWORK"
|
|
34
|
+
| "TIMEOUT"
|
|
35
|
+
| "HTTP"
|
|
36
|
+
| "INVALID_JSON"
|
|
37
|
+
| "UNEXPECTED_RESPONSE";
|
|
38
|
+
|
|
39
|
+
export type LambdaConnectionState = {
|
|
40
|
+
status: LambdaConnectionStatus;
|
|
41
|
+
endpoint: string;
|
|
42
|
+
lastCheckedAt: string;
|
|
43
|
+
lastCheckPhase: LambdaConnectionPhase;
|
|
44
|
+
errorCode?: LambdaConnectionErrorCode;
|
|
45
|
+
errorMessage?: string;
|
|
46
|
+
httpStatus?: number;
|
|
47
|
+
responseSnippet?: string;
|
|
48
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import binCleanup from "./binCleanup";
|
|
3
|
+
import { cleanupRecordBinWithoutLambda } from "./lambdaLessCleanup";
|
|
4
|
+
|
|
5
|
+
vi.mock("./lambdaLessCleanup", () => ({
|
|
6
|
+
cleanupRecordBinWithoutLambda: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const createCtxMock = (
|
|
10
|
+
parameters: Record<string, unknown>
|
|
11
|
+
): {
|
|
12
|
+
plugin: {
|
|
13
|
+
attributes: {
|
|
14
|
+
parameters: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
environment: string;
|
|
18
|
+
currentUserAccessToken: string;
|
|
19
|
+
updatePluginParameters: ReturnType<typeof vi.fn>;
|
|
20
|
+
} => ({
|
|
21
|
+
plugin: {
|
|
22
|
+
attributes: {
|
|
23
|
+
parameters,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
environment: "main",
|
|
27
|
+
currentUserAccessToken: "token",
|
|
28
|
+
updatePluginParameters: vi.fn(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.restoreAllMocks();
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("binCleanup", () => {
|
|
37
|
+
it("uses lambda cleanup when deployment URL is configured", async () => {
|
|
38
|
+
const fetchMock = vi.fn().mockResolvedValue({ status: 200 });
|
|
39
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
40
|
+
|
|
41
|
+
const ctx = createCtxMock({
|
|
42
|
+
deploymentURL: "https://record-bin.example.com",
|
|
43
|
+
automaticBinCleanup: {
|
|
44
|
+
numberOfDays: 30,
|
|
45
|
+
timeStamp: "",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await binCleanup(ctx as never);
|
|
50
|
+
|
|
51
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
53
|
+
"https://record-bin.example.com",
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
method: "POST",
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
expect(cleanupRecordBinWithoutLambda).not.toHaveBeenCalled();
|
|
59
|
+
expect(ctx.updatePluginParameters).toHaveBeenCalledTimes(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("uses Lambda-less cleanup when no deployment URL is configured", async () => {
|
|
63
|
+
const fetchMock = vi.fn();
|
|
64
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
65
|
+
vi.mocked(cleanupRecordBinWithoutLambda).mockResolvedValue({
|
|
66
|
+
deletedCount: 2,
|
|
67
|
+
skipped: false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const ctx = createCtxMock({
|
|
71
|
+
automaticBinCleanup: {
|
|
72
|
+
numberOfDays: 10,
|
|
73
|
+
timeStamp: "",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await binCleanup(ctx as never);
|
|
78
|
+
|
|
79
|
+
expect(cleanupRecordBinWithoutLambda).toHaveBeenCalledWith({
|
|
80
|
+
currentUserAccessToken: "token",
|
|
81
|
+
environment: "main",
|
|
82
|
+
numberOfDays: 10,
|
|
83
|
+
});
|
|
84
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
85
|
+
expect(ctx.updatePluginParameters).toHaveBeenCalledTimes(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("skips cleanup if it already ran today", async () => {
|
|
89
|
+
const fetchMock = vi.fn();
|
|
90
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
91
|
+
|
|
92
|
+
const today = new Date().toISOString().split("T")[0];
|
|
93
|
+
const ctx = createCtxMock({
|
|
94
|
+
deploymentURL: "https://record-bin.example.com",
|
|
95
|
+
automaticBinCleanup: {
|
|
96
|
+
numberOfDays: 30,
|
|
97
|
+
timeStamp: today,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await binCleanup(ctx as never);
|
|
102
|
+
|
|
103
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
104
|
+
expect(cleanupRecordBinWithoutLambda).not.toHaveBeenCalled();
|
|
105
|
+
expect(ctx.updatePluginParameters).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/utils/binCleanup.ts
CHANGED
|
@@ -1,42 +1,90 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OnBootCtx } from "datocms-plugin-sdk";
|
|
2
2
|
import { automaticBinCleanupObject } from "../types/types";
|
|
3
|
+
import { createDebugLogger, isDebugEnabled } from "./debugLogger";
|
|
4
|
+
import { getDeploymentUrlFromParameters } from "./getDeploymentUrlFromParameters";
|
|
5
|
+
import { getRuntimeMode } from "./getRuntimeMode";
|
|
6
|
+
import { cleanupRecordBinWithoutLambda } from "./lambdaLessCleanup";
|
|
7
|
+
|
|
8
|
+
const binCleanup = async (ctx: OnBootCtx) => {
|
|
9
|
+
const debugLogger = createDebugLogger(
|
|
10
|
+
isDebugEnabled(ctx.plugin.attributes.parameters),
|
|
11
|
+
"binCleanup"
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
debugLogger.log("Evaluating daily bin cleanup execution");
|
|
3
15
|
|
|
4
|
-
const binCleanup = async (ctx: OnBootPropertiesAndMethods) => {
|
|
5
16
|
if (ctx.plugin.attributes.parameters.automaticBinCleanup) {
|
|
6
17
|
const currentTimeStamp = new Date().toISOString().split("T")[0];
|
|
18
|
+
const cleanupSettings = ctx.plugin.attributes.parameters
|
|
19
|
+
.automaticBinCleanup as automaticBinCleanupObject;
|
|
7
20
|
if (
|
|
8
|
-
|
|
9
|
-
ctx.plugin.attributes.parameters
|
|
10
|
-
.automaticBinCleanup as automaticBinCleanupObject
|
|
11
|
-
).timeStamp === currentTimeStamp
|
|
21
|
+
cleanupSettings.timeStamp === currentTimeStamp
|
|
12
22
|
) {
|
|
23
|
+
debugLogger.log("Skipping cleanup because it already ran today", {
|
|
24
|
+
currentTimeStamp,
|
|
25
|
+
});
|
|
13
26
|
return;
|
|
14
27
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
|
|
29
|
+
const runtimeMode = getRuntimeMode(ctx.plugin.attributes.parameters);
|
|
30
|
+
|
|
31
|
+
if (runtimeMode === "lambda") {
|
|
32
|
+
const deploymentURL = getDeploymentUrlFromParameters(
|
|
18
33
|
ctx.plugin.attributes.parameters
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
);
|
|
35
|
+
if (!deploymentURL) {
|
|
36
|
+
debugLogger.warn("Skipping cleanup because deployment URL is missing");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const requestBody = {
|
|
41
|
+
event_type: "cleanup",
|
|
42
|
+
numberOfDays: cleanupSettings.numberOfDays,
|
|
43
|
+
environment: ctx.environment,
|
|
44
|
+
};
|
|
45
|
+
const parsedBody = JSON.stringify(requestBody);
|
|
46
|
+
|
|
47
|
+
debugLogger.log("Sending lambda cleanup request", {
|
|
48
|
+
deploymentURL,
|
|
49
|
+
requestBody,
|
|
31
50
|
});
|
|
32
|
-
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(deploymentURL, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: parsedBody,
|
|
56
|
+
headers: { Accept: "*/*", "Content-Type": "application/json" },
|
|
57
|
+
});
|
|
58
|
+
debugLogger.log("Lambda cleanup request finished", {
|
|
59
|
+
status: response.status,
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
debugLogger.error("Lambda cleanup request failed", error);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
debugLogger.log("Running cleanup in Lambda-less mode");
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const cleanupResult = await cleanupRecordBinWithoutLambda({
|
|
69
|
+
currentUserAccessToken: ctx.currentUserAccessToken,
|
|
70
|
+
environment: ctx.environment,
|
|
71
|
+
numberOfDays: cleanupSettings.numberOfDays,
|
|
72
|
+
});
|
|
73
|
+
debugLogger.log("Lambda-less cleanup finished", cleanupResult);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
debugLogger.error("Lambda-less cleanup failed", error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
33
78
|
|
|
34
79
|
const newParameters = { ...ctx.plugin.attributes.parameters };
|
|
35
80
|
(newParameters.automaticBinCleanup as automaticBinCleanupObject).timeStamp =
|
|
36
81
|
currentTimeStamp;
|
|
37
|
-
|
|
38
82
|
await ctx.updatePluginParameters(newParameters);
|
|
83
|
+
debugLogger.log("Cleanup timestamp persisted", { currentTimeStamp });
|
|
84
|
+
return;
|
|
39
85
|
}
|
|
86
|
+
|
|
87
|
+
debugLogger.log("Skipping cleanup because automatic cleanup is disabled");
|
|
40
88
|
};
|
|
41
89
|
|
|
42
90
|
export default binCleanup;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type PluginParameters = Record<string, unknown> | undefined;
|
|
2
|
+
|
|
3
|
+
type LogMethod = "log" | "warn" | "error";
|
|
4
|
+
|
|
5
|
+
const LOG_PREFIX = "[record-bin]";
|
|
6
|
+
|
|
7
|
+
export const isDebugEnabled = (parameters: PluginParameters): boolean =>
|
|
8
|
+
parameters?.debug === true;
|
|
9
|
+
|
|
10
|
+
export const createDebugLogger = (debugEnabled: boolean, scope: string) => {
|
|
11
|
+
const prefix = `${LOG_PREFIX}[${scope}]`;
|
|
12
|
+
|
|
13
|
+
const write = (method: LogMethod, ...args: unknown[]) => {
|
|
14
|
+
if (!debugEnabled) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console[method](prefix, ...args);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
enabled: debugEnabled,
|
|
23
|
+
log: (...args: unknown[]) => write("log", ...args),
|
|
24
|
+
warn: (...args: unknown[]) => write("warn", ...args),
|
|
25
|
+
error: (...args: unknown[]) => write("error", ...args),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { DEPLOY_PROVIDER_OPTIONS, PLUGIN_README_URL } from "./deployProviders";
|
|
3
|
+
|
|
4
|
+
describe("DEPLOY_PROVIDER_OPTIONS", () => {
|
|
5
|
+
it("contains the expected providers, labels, and urls", () => {
|
|
6
|
+
expect(DEPLOY_PROVIDER_OPTIONS).toEqual([
|
|
7
|
+
{
|
|
8
|
+
provider: "vercel",
|
|
9
|
+
label: "Vercel",
|
|
10
|
+
url: "https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmarcelofinamorvieira%2Frecord-bin-lambda-function&env=DATOCMS_FULLACCESS_API_TOKEN&project-name=datocms-record-bin-lambda-function&repo-name=datocms-record-bin-lambda-function",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
provider: "netlify",
|
|
14
|
+
label: "Netlify",
|
|
15
|
+
url: "https://app.netlify.com/start/deploy?repository=https://github.com/marcelofinamorvieira/record-bin-lambda-function",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
provider: "cloudflare",
|
|
19
|
+
label: "Cloudflare",
|
|
20
|
+
url: "https://github.com/marcelofinamorvieira/record-bin-lambda-function#deploying-on-cloudflare-workers",
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("PLUGIN_README_URL", () => {
|
|
27
|
+
it("is a valid absolute URL", () => {
|
|
28
|
+
expect(() => new URL(PLUGIN_README_URL)).not.toThrow();
|
|
29
|
+
expect(PLUGIN_README_URL).toBe(
|
|
30
|
+
"https://github.com/datocms/plugins/tree/master/record-bin#readme"
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type DeployProvider = "vercel" | "netlify" | "cloudflare";
|
|
2
|
+
|
|
3
|
+
export type DeployProviderOption = {
|
|
4
|
+
provider: DeployProvider;
|
|
5
|
+
label: string;
|
|
6
|
+
url: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const DEPLOY_PROVIDER_OPTIONS: DeployProviderOption[] = [
|
|
10
|
+
{
|
|
11
|
+
provider: "vercel",
|
|
12
|
+
label: "Vercel",
|
|
13
|
+
url: "https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmarcelofinamorvieira%2Frecord-bin-lambda-function&env=DATOCMS_FULLACCESS_API_TOKEN&project-name=datocms-record-bin-lambda-function&repo-name=datocms-record-bin-lambda-function",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
provider: "netlify",
|
|
17
|
+
label: "Netlify",
|
|
18
|
+
url: "https://app.netlify.com/start/deploy?repository=https://github.com/marcelofinamorvieira/record-bin-lambda-function",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
provider: "cloudflare",
|
|
22
|
+
label: "Cloudflare",
|
|
23
|
+
url: "https://github.com/marcelofinamorvieira/record-bin-lambda-function#deploying-on-cloudflare-workers",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const PLUGIN_README_URL =
|
|
28
|
+
"https://github.com/datocms/plugins/tree/master/record-bin#readme";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getDeploymentUrlFromParameters } from "./getDeploymentUrlFromParameters";
|
|
3
|
+
|
|
4
|
+
describe("getDeploymentUrlFromParameters", () => {
|
|
5
|
+
it("prefers deploymentURL when present", () => {
|
|
6
|
+
const result = getDeploymentUrlFromParameters({
|
|
7
|
+
deploymentURL: "https://record-bin.example.com",
|
|
8
|
+
vercelURL: "https://record-bin.vercel.app",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(result).toBe("https://record-bin.example.com");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("falls back to legacy vercelURL", () => {
|
|
15
|
+
const result = getDeploymentUrlFromParameters({
|
|
16
|
+
vercelURL: "https://record-bin.vercel.app",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(result).toBe("https://record-bin.vercel.app");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns empty string when no URL is configured", () => {
|
|
23
|
+
expect(getDeploymentUrlFromParameters(undefined)).toBe("");
|
|
24
|
+
expect(getDeploymentUrlFromParameters({})).toBe("");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type PluginParameters = Record<string, unknown> | undefined;
|
|
2
|
+
|
|
3
|
+
const isString = (value: unknown): value is string => typeof value === "string";
|
|
4
|
+
|
|
5
|
+
export const getDeploymentUrlFromParameters = (
|
|
6
|
+
parameters: PluginParameters
|
|
7
|
+
): string => {
|
|
8
|
+
if (!parameters) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (isString(parameters.deploymentURL) && parameters.deploymentURL.trim()) {
|
|
13
|
+
return parameters.deploymentURL;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (isString(parameters.vercelURL)) {
|
|
17
|
+
return parameters.vercelURL;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return "";
|
|
21
|
+
};
|