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.
Files changed (47) hide show
  1. package/README.md +127 -11
  2. package/build/assets/index-BnrW9Ts8.js +15 -0
  3. package/build/assets/index-aWCW2c0n.css +1 -0
  4. package/build/index.html +13 -1
  5. package/index.html +12 -0
  6. package/package.json +24 -18
  7. package/src/entrypoints/BinOutlet.tsx +262 -37
  8. package/src/entrypoints/ConfigScreen.tsx +939 -38
  9. package/src/entrypoints/ErrorModal.tsx +86 -2
  10. package/src/index.tsx +73 -28
  11. package/src/react-app-env.d.ts +1 -1
  12. package/src/types/types.ts +36 -8
  13. package/src/utils/binCleanup.test.ts +107 -0
  14. package/src/utils/binCleanup.ts +71 -23
  15. package/src/utils/debugLogger.ts +27 -0
  16. package/src/utils/deployProviders.test.ts +33 -0
  17. package/src/utils/deployProviders.ts +28 -0
  18. package/src/utils/getDeploymentUrlFromParameters.test.ts +26 -0
  19. package/src/utils/getDeploymentUrlFromParameters.ts +21 -0
  20. package/src/utils/getRuntimeMode.test.ts +57 -0
  21. package/src/utils/getRuntimeMode.ts +23 -0
  22. package/src/utils/lambdaLessCapture.test.ts +218 -0
  23. package/src/utils/lambdaLessCapture.ts +160 -0
  24. package/src/utils/lambdaLessCleanup.test.ts +125 -0
  25. package/src/utils/lambdaLessCleanup.ts +69 -0
  26. package/src/utils/lambdaLessRestore.test.ts +248 -0
  27. package/src/utils/lambdaLessRestore.ts +159 -0
  28. package/src/utils/recordBinModel.ts +108 -0
  29. package/src/utils/recordBinPayload.test.ts +103 -0
  30. package/src/utils/recordBinPayload.ts +136 -0
  31. package/src/utils/recordBinWebhook.test.ts +253 -0
  32. package/src/utils/recordBinWebhook.ts +305 -0
  33. package/src/utils/render.tsx +17 -8
  34. package/src/utils/restoreError.test.ts +112 -0
  35. package/src/utils/restoreError.ts +221 -0
  36. package/src/utils/verifyLambdaHealth.test.ts +248 -0
  37. package/src/utils/verifyLambdaHealth.ts +422 -0
  38. package/vite.config.ts +11 -0
  39. package/build/asset-manifest.json +0 -13
  40. package/build/static/css/main.10f29737.css +0 -2
  41. package/build/static/css/main.10f29737.css.map +0 -1
  42. package/build/static/js/main.53795e3b.js +0 -3
  43. package/build/static/js/main.53795e3b.js.LICENSE.txt +0 -47
  44. package/build/static/js/main.53795e3b.js.map +0 -1
  45. package/src/entrypoints/InstallationModal.tsx +0 -107
  46. package/src/entrypoints/PreInstallConfig.tsx +0 -28
  47. 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 newString = (ctx.parameters.errorPayload as string)
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
- <pre>{newString}</pre>
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
- IntentCtx,
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
- if (
20
- !ctx.plugin.attributes.parameters.installationState &&
21
- !ctx.plugin.attributes.parameters.hasBeenPrompted
22
- ) {
23
- ctx.updatePluginParameters({ hasBeenPrompted: true });
24
- await ctx.openModal({
25
- id: "installationModal",
26
- title: "Record Bin setup",
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
- renderConfigScreen(ctx) {
36
- if (ctx.plugin.attributes.parameters.installationState === "installed") {
37
- return render(<ConfigScreen ctx={ctx} />);
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
- return render(<PreInstallConfig ctx={ctx} />);
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, ctx: IntentCtx) {
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
- if (
54
- outletId === "recordBin" &&
55
- ctx.plugin.attributes.parameters.installationState === "installed"
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
  });
@@ -1 +1 @@
1
- /// <reference types="react-scripts" />
1
+ /// <reference types="vite/client" />
@@ -1,15 +1,17 @@
1
1
  export type errorObject = {
2
2
  simplifiedError: {
3
- code: string;
3
+ code?: string;
4
4
  details: {
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;
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
+ });
@@ -1,42 +1,90 @@
1
- import { OnBootPropertiesAndMethods } from "datocms-plugin-sdk";
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
- const requestBody = {
16
- event_type: "cleanup",
17
- numberOfDays: (
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
- .automaticBinCleanup as automaticBinCleanupObject
20
- ).numberOfDays,
21
- environment: ctx.environment,
22
- };
23
-
24
- const parsedBody = JSON.stringify(requestBody);
25
-
26
- try {
27
- await fetch(ctx.plugin.attributes.parameters.vercelURL as URL, {
28
- method: "POST",
29
- body: parsedBody,
30
- headers: { Accept: "*/*", "Content-Type": "application/json" },
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
- } catch {}
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
+ };