@spindlecommerce/plugin-sdk 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -2
- package/dist/errors.d.ts +62 -0
- package/dist/index.d.ts +623 -0
- package/dist/index.js +59 -0
- package/dist/internal.d.ts +32 -0
- package/dist/refs.d.ts +45 -0
- package/package.json +32 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The SpindleCommerce authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @spindlecommerce/plugin-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The plugin API for [SpindleCommerce](https://spindlecommerce.com). A plugin adds to the platform
|
|
4
|
+
without changing its code: a payment or shipping provider, an email or ERP integration, a fraud
|
|
5
|
+
check, GraphQL fields, HTTP routes or scheduled tasks. Staff switch plugins on and fill in their
|
|
6
|
+
settings in the admin, under **Settings › Plugins**.
|
|
7
|
+
|
|
8
|
+
## Writing a plugin
|
|
9
|
+
|
|
10
|
+
The quickest start is the template,
|
|
11
|
+
[spindlecommerce/plugin-hello-world](https://github.com/spindlecommerce/plugin-hello-world). By hand:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add -D @spindlecommerce/plugin-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```jsonc
|
|
18
|
+
// package.json
|
|
19
|
+
"peerDependencies": { "@spindlecommerce/plugin-sdk": "^1.0.0" }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { definePlugin, setting } from "@spindlecommerce/plugin-sdk";
|
|
24
|
+
|
|
25
|
+
export const hello = () =>
|
|
26
|
+
definePlugin({
|
|
27
|
+
name: "hello",
|
|
28
|
+
version: "1.0.0",
|
|
29
|
+
apiVersion: 1,
|
|
30
|
+
title: "Hello",
|
|
31
|
+
settings: { greeting: setting.string({ label: "Greeting", default: "Hello" }) },
|
|
32
|
+
events: {
|
|
33
|
+
"order.placed": async (event, ctx) => ctx.logger.info({ orderId: event.aggregateId }, ctx.settings.greeting),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **The peer dependency matters.** In an installation, `@spindlecommerce/plugin-sdk` is the
|
|
39
|
+
platform's own SDK, and the peer dependency makes your plugin use it. Then errors such as
|
|
40
|
+
`PaymentDeclinedError` are the ones the platform checks for.
|
|
41
|
+
- **Write `apiVersion` as a number.** Inside an installation `PLUGIN_API_VERSION` is the platform's
|
|
42
|
+
own, so a plugin that used it would always claim to match.
|
|
43
|
+
- **Publish JavaScript.** Build with `tsc` to `dist/`. Installations load plugins from
|
|
44
|
+
`node_modules`, where Node does not strip TypeScript types.
|
|
45
|
+
- **Handlers run at least once.** An event can be delivered again after a timeout. Use `ctx.once`
|
|
46
|
+
for anything that must happen once, such as sending an email.
|
|
47
|
+
|
|
48
|
+
## Installing a plugin
|
|
49
|
+
|
|
50
|
+
In an installation:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pnpm add @spindlecommerce/plugin-hello-world
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// ecom.config.ts
|
|
58
|
+
import { helloWorld } from "@spindlecommerce/plugin-hello-world";
|
|
59
|
+
|
|
60
|
+
export default defineConfig({ plugins: [stripe(), email(), helloWorld()] });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Deploy, then open **Settings › Plugins**. A plugin starts working once every required setting has a
|
|
64
|
+
value.
|
|
65
|
+
|
|
66
|
+
## What's in it
|
|
67
|
+
|
|
68
|
+
- `definePlugin`, `defineConfig`, `setting` and `PLUGIN_API_VERSION`.
|
|
69
|
+
- The errors plugins throw: `ValidationError`, `NotFoundError`, `InvalidStateError` and
|
|
70
|
+
`PaymentDeclinedError`.
|
|
71
|
+
- Types for every extension point: events, the checkout hook, payment and shipping providers, HTTP
|
|
72
|
+
routes, scheduled tasks, migrations and GraphQL.
|
|
73
|
+
- The handler context, `PluginContext`: settings, logger, key-value storage, `once`, `emit`, `fetch`
|
|
74
|
+
and the platform's services.
|
|
75
|
+
|
|
76
|
+
`ctx.db` is the platform's PostgreSQL database (Drizzle), for your plugin's own tables. In this
|
|
77
|
+
package, `ctx.platform` and the GraphQL builder, refs and context have loose types. They are internal
|
|
78
|
+
to the platform, and in an installation they have its precise types.
|
|
79
|
+
|
|
80
|
+
## Versions
|
|
81
|
+
|
|
82
|
+
The major version is the plugin API version: 1.x is `apiVersion: 1`. A platform refuses, at startup,
|
|
83
|
+
a plugin written for another API version. Minor versions add to the API without breaking plugins.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expected failures. Services throw these; the GraphQL layer turns each class into a typed
|
|
3
|
+
* error object in the mutation's result union instead of an untyped top-level error.
|
|
4
|
+
*/
|
|
5
|
+
export declare class DomainError extends Error {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
constructor(code: string, message: string);
|
|
8
|
+
}
|
|
9
|
+
export type FieldError = {
|
|
10
|
+
path: string[];
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
export declare class ValidationError extends DomainError {
|
|
14
|
+
readonly fieldErrors: FieldError[];
|
|
15
|
+
constructor(message: string, fieldErrors?: FieldError[]);
|
|
16
|
+
}
|
|
17
|
+
export declare class NotFoundError extends DomainError {
|
|
18
|
+
readonly resource: string;
|
|
19
|
+
readonly lookup: string;
|
|
20
|
+
constructor(resource: string, lookup: string);
|
|
21
|
+
}
|
|
22
|
+
export declare class ConflictError extends DomainError {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
25
|
+
export declare class AuthenticationError extends DomainError {
|
|
26
|
+
constructor(message?: string);
|
|
27
|
+
}
|
|
28
|
+
export declare class PermissionDeniedError extends DomainError {
|
|
29
|
+
readonly permission: string;
|
|
30
|
+
constructor(permission: string);
|
|
31
|
+
}
|
|
32
|
+
export declare class InsufficientStockError extends DomainError {
|
|
33
|
+
readonly variantId: string;
|
|
34
|
+
readonly sku: string;
|
|
35
|
+
readonly requested: number;
|
|
36
|
+
readonly available: number;
|
|
37
|
+
constructor(input: {
|
|
38
|
+
variantId: string;
|
|
39
|
+
sku: string;
|
|
40
|
+
requested: number;
|
|
41
|
+
available: number;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export declare class CouponNotApplicableError extends DomainError {
|
|
45
|
+
readonly couponCode: string;
|
|
46
|
+
readonly reason: string;
|
|
47
|
+
constructor(couponCode: string, reason: string);
|
|
48
|
+
}
|
|
49
|
+
export declare class CartNotReadyError extends DomainError {
|
|
50
|
+
readonly missing: string[];
|
|
51
|
+
/** The same, field by field. */
|
|
52
|
+
readonly fieldErrors: FieldError[];
|
|
53
|
+
constructor(missing: string[], fieldErrors?: FieldError[]);
|
|
54
|
+
}
|
|
55
|
+
export declare class PaymentDeclinedError extends DomainError {
|
|
56
|
+
readonly declineCode: string;
|
|
57
|
+
constructor(declineCode: string, message: string);
|
|
58
|
+
}
|
|
59
|
+
/** The entity exists but is in a state that does not allow the operation (e.g. refunding more than captured). */
|
|
60
|
+
export declare class InvalidStateError extends DomainError {
|
|
61
|
+
constructor(message: string);
|
|
62
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin API. A plugin is a value made with `definePlugin`, listed in ecom.config.ts and bundled
|
|
3
|
+
* with the app, so it runs wherever the platform does, serverless hosts included. Staff enable it
|
|
4
|
+
* and fill in its settings in the admin (Settings › Plugins).
|
|
5
|
+
*
|
|
6
|
+
* Plugins import this module, as @spindlecommerce/plugin-sdk, and nothing else of the platform: it
|
|
7
|
+
* is the surface kept stable across releases (`apiVersion`). The rest of the code base is internal;
|
|
8
|
+
* `ctx.platform` reaches it when a plugin must, at the cost of that promise.
|
|
9
|
+
*
|
|
10
|
+
* Extension points are typed and explicit (no patching of core code, so two plugins cannot fight
|
|
11
|
+
* over a method):
|
|
12
|
+
* - events: domain events after they commit, delivered at least once with retries;
|
|
13
|
+
* - hooks: a few synchronous checks on hot paths (checkout), each time-limited;
|
|
14
|
+
* - payments and shipping: providers that payment and shipping methods use;
|
|
15
|
+
* - graphql: types and fields on the admin or storefront API;
|
|
16
|
+
* - routes: HTTP endpoints under /plugins/<name>/, e.g. for webhooks from other services;
|
|
17
|
+
* - tasks: periodic jobs run by the worker;
|
|
18
|
+
* - migrations: SQL for the plugin's own tables;
|
|
19
|
+
* - admin: pages, navigation entries and widgets bundled into the admin app.
|
|
20
|
+
*/
|
|
21
|
+
import type { Context as HonoContext, Hono } from "hono";
|
|
22
|
+
import type { Logger } from "pino";
|
|
23
|
+
import type { Database } from "./internal.js";
|
|
24
|
+
import type { builder as Builder, SubGraph } from "./internal.js";
|
|
25
|
+
import type { GraphQLContext } from "./internal.js";
|
|
26
|
+
import type * as Refs from "./refs.js";
|
|
27
|
+
import type { Address } from "./internal.js";
|
|
28
|
+
import type { Platform } from "./internal.js";
|
|
29
|
+
export { InvalidStateError, NotFoundError, PaymentDeclinedError, ValidationError } from "./errors.js";
|
|
30
|
+
export type { Address };
|
|
31
|
+
/** The plugin API this platform implements. A plugin built for another version is refused at startup. */
|
|
32
|
+
export declare const PLUGIN_API_VERSION = 1;
|
|
33
|
+
type FieldBase = {
|
|
34
|
+
label: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
/**
|
|
37
|
+
* An environment variable that supplies the value instead (e.g. STRIPE_SECRET_KEY). When it is
|
|
38
|
+
* set, the admin shows the field as set by the environment and read-only.
|
|
39
|
+
*/
|
|
40
|
+
env?: string;
|
|
41
|
+
};
|
|
42
|
+
export type StringField = FieldBase & {
|
|
43
|
+
type: "string";
|
|
44
|
+
required?: boolean;
|
|
45
|
+
default?: string;
|
|
46
|
+
placeholder?: string;
|
|
47
|
+
multiline?: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** Encrypted at rest and write-only: the admin API reports whether it is set, never the value. */
|
|
50
|
+
export type SecretField = FieldBase & {
|
|
51
|
+
type: "secret";
|
|
52
|
+
required?: boolean;
|
|
53
|
+
};
|
|
54
|
+
export type BooleanField = FieldBase & {
|
|
55
|
+
type: "boolean";
|
|
56
|
+
default?: boolean;
|
|
57
|
+
};
|
|
58
|
+
export type NumberField = FieldBase & {
|
|
59
|
+
type: "number";
|
|
60
|
+
required?: boolean;
|
|
61
|
+
default?: number;
|
|
62
|
+
min?: number;
|
|
63
|
+
max?: number;
|
|
64
|
+
integer?: boolean;
|
|
65
|
+
};
|
|
66
|
+
export type SelectField<V extends string = string> = FieldBase & {
|
|
67
|
+
type: "select";
|
|
68
|
+
options: readonly {
|
|
69
|
+
value: V;
|
|
70
|
+
label: string;
|
|
71
|
+
}[];
|
|
72
|
+
required?: boolean;
|
|
73
|
+
default?: V;
|
|
74
|
+
};
|
|
75
|
+
export type SettingField = StringField | SecretField | BooleanField | NumberField | SelectField;
|
|
76
|
+
export type SettingsShape = Record<string, SettingField>;
|
|
77
|
+
type Present<F> = F extends {
|
|
78
|
+
required: true;
|
|
79
|
+
} ? true : F extends {
|
|
80
|
+
default: unknown;
|
|
81
|
+
} ? true : false;
|
|
82
|
+
type FieldValue<F> = F extends {
|
|
83
|
+
type: "boolean";
|
|
84
|
+
} ? boolean : F extends {
|
|
85
|
+
type: "number";
|
|
86
|
+
} ? Present<F> extends true ? number : number | undefined : F extends SelectField<infer V> ? Present<F> extends true ? V : V | undefined : Present<F> extends true ? string : string | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Settings as plugin code sees them. Required fields are always present: the platform runs a
|
|
89
|
+
* plugin's handlers and providers only once every required setting has a value ("configured").
|
|
90
|
+
*/
|
|
91
|
+
export type SettingsValues<S extends SettingsShape> = {
|
|
92
|
+
readonly [K in keyof S]: FieldValue<S[K]>;
|
|
93
|
+
};
|
|
94
|
+
/** Field builders: `settings: { apiKey: setting.secret({ label: "API key", required: true, env: "ACME_API_KEY" }) }`. */
|
|
95
|
+
export declare const setting: {
|
|
96
|
+
string: <const F extends Omit<StringField, "type">>(field: F) => F & {
|
|
97
|
+
type: "string";
|
|
98
|
+
};
|
|
99
|
+
secret: <const F extends Omit<SecretField, "type">>(field: F) => F & {
|
|
100
|
+
type: "secret";
|
|
101
|
+
};
|
|
102
|
+
boolean: <const F extends Omit<BooleanField, "type">>(field: F) => F & {
|
|
103
|
+
type: "boolean";
|
|
104
|
+
};
|
|
105
|
+
number: <const F extends Omit<NumberField, "type">>(field: F) => F & {
|
|
106
|
+
type: "number";
|
|
107
|
+
};
|
|
108
|
+
select: <const V extends string, const F extends Omit<SelectField<V>, "type">>(field: F & {
|
|
109
|
+
options: readonly {
|
|
110
|
+
value: V;
|
|
111
|
+
label: string;
|
|
112
|
+
}[];
|
|
113
|
+
}) => F & {
|
|
114
|
+
options: readonly {
|
|
115
|
+
value: V;
|
|
116
|
+
label: string;
|
|
117
|
+
}[];
|
|
118
|
+
type: "select";
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
/** Plugin-scoped storage (one namespace per plugin), for state that needs no table of its own. */
|
|
122
|
+
export type PluginKeyValueStore = {
|
|
123
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
124
|
+
set(key: string, value: unknown, options?: {
|
|
125
|
+
ttlSeconds?: number;
|
|
126
|
+
}): Promise<void>;
|
|
127
|
+
/** Set only when absent; returns whether it was set. */
|
|
128
|
+
add(key: string, value: unknown, options?: {
|
|
129
|
+
ttlSeconds?: number;
|
|
130
|
+
}): Promise<boolean>;
|
|
131
|
+
delete(key: string): Promise<void>;
|
|
132
|
+
/** Entries whose key starts with `prefix`, in key order. */
|
|
133
|
+
list<T = unknown>(prefix: string, options?: {
|
|
134
|
+
limit?: number;
|
|
135
|
+
}): Promise<{
|
|
136
|
+
key: string;
|
|
137
|
+
value: T;
|
|
138
|
+
}[]>;
|
|
139
|
+
};
|
|
140
|
+
export type TrackingNumber = {
|
|
141
|
+
carrier: string;
|
|
142
|
+
number: string;
|
|
143
|
+
url?: string | null;
|
|
144
|
+
};
|
|
145
|
+
/** An order as plugins see it. Amounts are integer minor units in the order's currency. */
|
|
146
|
+
export type PluginOrder = {
|
|
147
|
+
id: string;
|
|
148
|
+
number: string;
|
|
149
|
+
channelId: string;
|
|
150
|
+
customerId: string | null;
|
|
151
|
+
email: string;
|
|
152
|
+
locale: string;
|
|
153
|
+
currency: string;
|
|
154
|
+
status: string;
|
|
155
|
+
paymentStatus: string;
|
|
156
|
+
fulfillmentStatus: string;
|
|
157
|
+
subtotal: number;
|
|
158
|
+
discountTotal: number;
|
|
159
|
+
shippingTotal: number;
|
|
160
|
+
taxTotal: number;
|
|
161
|
+
grandTotal: number;
|
|
162
|
+
pricesIncludeTax: boolean;
|
|
163
|
+
shippingAddress: Address | null;
|
|
164
|
+
billingAddress: Address | null;
|
|
165
|
+
/** The shipping rate chosen at checkout; `details` comes from its provider (see RateQuote.data). */
|
|
166
|
+
shippingMethod: {
|
|
167
|
+
code: string;
|
|
168
|
+
name: string;
|
|
169
|
+
carrier: string;
|
|
170
|
+
amount: number;
|
|
171
|
+
details?: ShippingRateDetails | null;
|
|
172
|
+
} | null;
|
|
173
|
+
paymentMethodCode: string;
|
|
174
|
+
lines: {
|
|
175
|
+
id: string;
|
|
176
|
+
productId: string | null;
|
|
177
|
+
variantId: string | null;
|
|
178
|
+
sku: string;
|
|
179
|
+
name: string;
|
|
180
|
+
variantAttributes: Record<string, string>;
|
|
181
|
+
quantity: number;
|
|
182
|
+
quantityFulfilled: number;
|
|
183
|
+
unitPrice: number;
|
|
184
|
+
total: number;
|
|
185
|
+
weightGrams: number | null;
|
|
186
|
+
isShippable: boolean;
|
|
187
|
+
}[];
|
|
188
|
+
fulfillments: {
|
|
189
|
+
id: string;
|
|
190
|
+
number: string;
|
|
191
|
+
tracking: TrackingNumber[];
|
|
192
|
+
lines: {
|
|
193
|
+
orderLineId: string;
|
|
194
|
+
quantity: number;
|
|
195
|
+
}[];
|
|
196
|
+
createdAt: Date;
|
|
197
|
+
}[];
|
|
198
|
+
metadata: Record<string, unknown>;
|
|
199
|
+
createdAt: Date;
|
|
200
|
+
};
|
|
201
|
+
export type PluginChannel = {
|
|
202
|
+
id: string;
|
|
203
|
+
code: string;
|
|
204
|
+
name: string;
|
|
205
|
+
defaultLocale: string;
|
|
206
|
+
storefrontUrl: string | null;
|
|
207
|
+
};
|
|
208
|
+
/** Read models and a few operations, stable across releases. */
|
|
209
|
+
export type PluginServices = {
|
|
210
|
+
orders: {
|
|
211
|
+
get(id: string): Promise<PluginOrder | null>;
|
|
212
|
+
getByNumber(number: string): Promise<PluginOrder | null>;
|
|
213
|
+
/** A note in the order's history, attributed to the plugin. */
|
|
214
|
+
addNote(orderId: string, message: string): Promise<void>;
|
|
215
|
+
};
|
|
216
|
+
customers: {
|
|
217
|
+
/** `locale`: the one they last shopped in (their latest order's, else their latest cart's); null when they have neither. */
|
|
218
|
+
get(id: string): Promise<{
|
|
219
|
+
id: string;
|
|
220
|
+
email: string;
|
|
221
|
+
firstName: string | null;
|
|
222
|
+
lastName: string | null;
|
|
223
|
+
locale: string | null;
|
|
224
|
+
} | null>;
|
|
225
|
+
};
|
|
226
|
+
channels: {
|
|
227
|
+
/** `storefrontUrl`: the first storefront registered for the channel (Settings › Storefronts), if any. */
|
|
228
|
+
get(id: string): Promise<PluginChannel | null>;
|
|
229
|
+
/**
|
|
230
|
+
* The channel a request that names none uses (DEFAULT_CHANNEL, else the first active channel),
|
|
231
|
+
* e.g. for messages to customers and staff, who belong to no channel.
|
|
232
|
+
*/
|
|
233
|
+
getDefault(): Promise<PluginChannel | null>;
|
|
234
|
+
};
|
|
235
|
+
locations: {
|
|
236
|
+
/** Where a channel's orders ship from: its first active stock location with an address. */
|
|
237
|
+
shipFrom(channelId: string): Promise<{
|
|
238
|
+
id: string;
|
|
239
|
+
name: string;
|
|
240
|
+
address: Partial<Address>;
|
|
241
|
+
} | null>;
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
export type PluginContext<S extends SettingsShape = SettingsShape> = {
|
|
245
|
+
plugin: {
|
|
246
|
+
name: string;
|
|
247
|
+
version: string;
|
|
248
|
+
};
|
|
249
|
+
settings: SettingsValues<S>;
|
|
250
|
+
db: Database;
|
|
251
|
+
/** A child logger tagged with the plugin's name. */
|
|
252
|
+
logger: Logger;
|
|
253
|
+
kv: PluginKeyValueStore;
|
|
254
|
+
/** The platform's fetch: use it rather than the global one, so tests can stand in for remote services. */
|
|
255
|
+
fetch: typeof fetch;
|
|
256
|
+
/**
|
|
257
|
+
* Run `fn` once per key across retries and instances (e.g. one confirmation email per order).
|
|
258
|
+
* Returns false when it had already run. A throwing `fn` does not count as run.
|
|
259
|
+
*/
|
|
260
|
+
once(key: string, fn: () => Promise<void>): Promise<boolean>;
|
|
261
|
+
/**
|
|
262
|
+
* Emit one of the plugin's declared events (`emits`). Pass the transaction's `db` to commit it with
|
|
263
|
+
* other writes; webhooks and other plugins receive it like a platform event.
|
|
264
|
+
*/
|
|
265
|
+
emit(event: {
|
|
266
|
+
type: string;
|
|
267
|
+
aggregateType: string;
|
|
268
|
+
aggregateId: string;
|
|
269
|
+
payload?: Record<string, unknown>;
|
|
270
|
+
}, db?: Database): Promise<void>;
|
|
271
|
+
services: PluginServices;
|
|
272
|
+
urls: {
|
|
273
|
+
/** The admin's public URL, e.g. https://shop-api.example.com/admin. */
|
|
274
|
+
admin: string;
|
|
275
|
+
/** This API's public base URL; the plugin's routes are at `${urls.api}/plugins/<name>/…`. */
|
|
276
|
+
api: string;
|
|
277
|
+
};
|
|
278
|
+
production: boolean;
|
|
279
|
+
/** Everything else, internal and not covered by `apiVersion`. */
|
|
280
|
+
platform: Platform;
|
|
281
|
+
};
|
|
282
|
+
/** A committed domain event: platform events (see EVENT_TYPES) and plugins' declared events. */
|
|
283
|
+
export type PluginEvent = {
|
|
284
|
+
id: string;
|
|
285
|
+
type: string;
|
|
286
|
+
aggregateType: string;
|
|
287
|
+
aggregateId: string;
|
|
288
|
+
payload: Record<string, unknown>;
|
|
289
|
+
occurredAt: Date;
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* Runs after the event commits, outside any request, at least once: a handler that throws is
|
|
293
|
+
* retried with backoff, so make it idempotent (see `ctx.once`). Order across events is not guaranteed.
|
|
294
|
+
*/
|
|
295
|
+
export type EventHandler<S extends SettingsShape = SettingsShape> = (event: PluginEvent, ctx: PluginContext<S>) => Promise<void>;
|
|
296
|
+
export type CheckoutValidateInput = {
|
|
297
|
+
cartId: string;
|
|
298
|
+
channelId: string;
|
|
299
|
+
customerId: string | null;
|
|
300
|
+
email: string | null;
|
|
301
|
+
currency: string;
|
|
302
|
+
grandTotal: number;
|
|
303
|
+
shippingAddress: Address | null;
|
|
304
|
+
billingAddress: Address | null;
|
|
305
|
+
shippingMethodCode: string | null;
|
|
306
|
+
paymentMethodCode: string | null;
|
|
307
|
+
lines: {
|
|
308
|
+
productId: string;
|
|
309
|
+
variantId: string;
|
|
310
|
+
sku: string;
|
|
311
|
+
quantity: number;
|
|
312
|
+
unitPrice: number;
|
|
313
|
+
}[];
|
|
314
|
+
};
|
|
315
|
+
export type CheckoutIssue = {
|
|
316
|
+
code: string;
|
|
317
|
+
message: string;
|
|
318
|
+
field?: string[];
|
|
319
|
+
};
|
|
320
|
+
export type PluginHooks<S extends SettingsShape> = {
|
|
321
|
+
/** Before an order is placed. Any issue refuses the order with a validation error. Limited to 5 seconds. */
|
|
322
|
+
checkoutValidate?: (input: CheckoutValidateInput, ctx: PluginContext<S>) => Promise<CheckoutIssue[]> | CheckoutIssue[];
|
|
323
|
+
};
|
|
324
|
+
export type PaymentSessionInput = {
|
|
325
|
+
cartId: string;
|
|
326
|
+
methodCode: string;
|
|
327
|
+
amount: number;
|
|
328
|
+
currency: string;
|
|
329
|
+
locale: string;
|
|
330
|
+
email: string | null;
|
|
331
|
+
customerId: string | null;
|
|
332
|
+
billingAddress: Address | null;
|
|
333
|
+
shippingAddress: Address | null;
|
|
334
|
+
lines: {
|
|
335
|
+
sku: string;
|
|
336
|
+
name: string;
|
|
337
|
+
quantity: number;
|
|
338
|
+
unitPrice: number;
|
|
339
|
+
total: number;
|
|
340
|
+
}[];
|
|
341
|
+
captureImmediately: boolean;
|
|
342
|
+
/** This cart's last session with the same provider: update it (e.g. a new amount) rather than start over. */
|
|
343
|
+
previous: Record<string, unknown> | null;
|
|
344
|
+
};
|
|
345
|
+
/** `data` stays on the server with the cart; `clientData` goes to the storefront (e.g. a client secret). */
|
|
346
|
+
export type PaymentSession = {
|
|
347
|
+
data: Record<string, unknown>;
|
|
348
|
+
clientData: Record<string, unknown>;
|
|
349
|
+
};
|
|
350
|
+
export type AuthorizeInput = {
|
|
351
|
+
orderId: string;
|
|
352
|
+
orderNumber: string;
|
|
353
|
+
cartId: string | null;
|
|
354
|
+
amount: number;
|
|
355
|
+
currency: string;
|
|
356
|
+
/**
|
|
357
|
+
* What the storefront sent with the order (e.g. a test token). When the platform places the
|
|
358
|
+
* order itself from a webhook (a customer who paid but never came back to the storefront), it is
|
|
359
|
+
* `{ pspReference }`: the payment the PSP reported.
|
|
360
|
+
*/
|
|
361
|
+
paymentData: Record<string, unknown>;
|
|
362
|
+
captureImmediately: boolean;
|
|
363
|
+
/** The cart's session `data`, when the provider created one. */
|
|
364
|
+
session: Record<string, unknown> | null;
|
|
365
|
+
};
|
|
366
|
+
/** Throw PaymentDeclinedError to decline: the order is canceled, its stock released and the cart reopened. */
|
|
367
|
+
export type AuthorizeResult = {
|
|
368
|
+
status: "authorized" | "captured" | "pending";
|
|
369
|
+
pspReference: string | null;
|
|
370
|
+
raw?: Record<string, unknown>;
|
|
371
|
+
};
|
|
372
|
+
export type ProviderResult = {
|
|
373
|
+
pspReference: string | null;
|
|
374
|
+
raw?: Record<string, unknown>;
|
|
375
|
+
};
|
|
376
|
+
/** A payment event a provider reports from a webhook. The platform applies each once, by `id`. */
|
|
377
|
+
export type PaymentWebhookEvent = {
|
|
378
|
+
/** The PSP's own event id, for idempotency. */
|
|
379
|
+
id: string;
|
|
380
|
+
type: "authorized" | "captured" | "failed" | "canceled" | "refunded" | "dispute_opened" | "dispute_closed";
|
|
381
|
+
/** The payment's reference (what authorize returned as pspReference). */
|
|
382
|
+
pspReference: string;
|
|
383
|
+
/** Required for `refunded`: the refund's own reference, so each refund is recorded once. */
|
|
384
|
+
refundReference?: string;
|
|
385
|
+
/** Minor units. For `captured`, the total captured so far (cumulative), not this capture alone. */
|
|
386
|
+
amount?: number;
|
|
387
|
+
currency?: string;
|
|
388
|
+
/** Where the PSP carries them (e.g. metadata): lets a payment completed without the storefront still become an order. */
|
|
389
|
+
orderId?: string;
|
|
390
|
+
cartId?: string;
|
|
391
|
+
reason?: string;
|
|
392
|
+
raw?: Record<string, unknown>;
|
|
393
|
+
};
|
|
394
|
+
/** `response` replaces the default 200 `{ "received": true }`. */
|
|
395
|
+
export type PaymentWebhookResult = {
|
|
396
|
+
events: PaymentWebhookEvent[];
|
|
397
|
+
response?: Response;
|
|
398
|
+
};
|
|
399
|
+
/**
|
|
400
|
+
* A payment service provider (PSP). Payment methods (Settings › Payments) pick a provider; the
|
|
401
|
+
* platform owns orders, transactions and invoices, the provider talks to the PSP.
|
|
402
|
+
*
|
|
403
|
+
* Checkout: the storefront may ask for a session (createSession) to render the PSP's own payment
|
|
404
|
+
* UI, then places the order; authorize then confirms the payment, normally by checking with the
|
|
405
|
+
* PSP what happened in the browser. Webhooks (POST /payments/webhooks/<code>) report what happens
|
|
406
|
+
* later, and complete orders whose customer never came back from the PSP.
|
|
407
|
+
*/
|
|
408
|
+
export interface PaymentProviderDefinition<S extends SettingsShape = SettingsShape> {
|
|
409
|
+
/** Unique across plugins, e.g. "stripe". */
|
|
410
|
+
readonly code: string;
|
|
411
|
+
readonly title: string;
|
|
412
|
+
readonly description?: string;
|
|
413
|
+
/** Offered at checkout only when this returns true (defaults to true). */
|
|
414
|
+
isAvailable?(input: {
|
|
415
|
+
currency: string;
|
|
416
|
+
countryCode: string | null;
|
|
417
|
+
amount: number;
|
|
418
|
+
}, ctx: PluginContext<S>): boolean | Promise<boolean>;
|
|
419
|
+
createSession?(input: PaymentSessionInput, ctx: PluginContext<S>): Promise<PaymentSession>;
|
|
420
|
+
authorize(input: AuthorizeInput, ctx: PluginContext<S>): Promise<AuthorizeResult>;
|
|
421
|
+
capture(input: {
|
|
422
|
+
orderId: string;
|
|
423
|
+
amount: number;
|
|
424
|
+
currency: string;
|
|
425
|
+
pspReference: string | null;
|
|
426
|
+
}, ctx: PluginContext<S>): Promise<ProviderResult>;
|
|
427
|
+
refund(input: {
|
|
428
|
+
orderId: string;
|
|
429
|
+
amount: number;
|
|
430
|
+
currency: string;
|
|
431
|
+
pspReference: string | null;
|
|
432
|
+
}, ctx: PluginContext<S>): Promise<ProviderResult>;
|
|
433
|
+
/**
|
|
434
|
+
* Cancel an order's authorization. An empty `orderId` means: release a session's payment that no
|
|
435
|
+
* order came of (stock ran out after the customer confirmed, a check refused the order). Give back
|
|
436
|
+
* whatever it secured, found by `pspReference` or the cart's session `data` (`session`), and leave
|
|
437
|
+
* an unpaid one usable.
|
|
438
|
+
*/
|
|
439
|
+
void(input: {
|
|
440
|
+
orderId: string;
|
|
441
|
+
pspReference: string | null;
|
|
442
|
+
session?: Record<string, unknown> | null;
|
|
443
|
+
}, ctx: PluginContext<S>): Promise<ProviderResult>;
|
|
444
|
+
/**
|
|
445
|
+
* Verify (signatures!) and translate a PSP webhook. Throw ValidationError when the request cannot
|
|
446
|
+
* be verified: the route answers 400 without details.
|
|
447
|
+
*/
|
|
448
|
+
webhook?(request: Request, ctx: PluginContext<S>): Promise<PaymentWebhookResult>;
|
|
449
|
+
}
|
|
450
|
+
export type RateRequest = {
|
|
451
|
+
/** The shipping method being priced; `options` are its provider options (ShippingProviderDefinition.methodOptions). */
|
|
452
|
+
method: {
|
|
453
|
+
id: string;
|
|
454
|
+
code: string;
|
|
455
|
+
name: string;
|
|
456
|
+
options: Record<string, unknown>;
|
|
457
|
+
};
|
|
458
|
+
channelId: string;
|
|
459
|
+
locale: string;
|
|
460
|
+
currency: string;
|
|
461
|
+
/** After discounts, minor units. */
|
|
462
|
+
subtotal: number;
|
|
463
|
+
weightGrams: number;
|
|
464
|
+
quantity: number;
|
|
465
|
+
destination: Address;
|
|
466
|
+
origin: Partial<Address> | null;
|
|
467
|
+
/** `weightGrams` and `unitPrice` are per unit. */
|
|
468
|
+
lines: {
|
|
469
|
+
sku: string;
|
|
470
|
+
name: string;
|
|
471
|
+
quantity: number;
|
|
472
|
+
weightGrams: number;
|
|
473
|
+
unitPrice: number;
|
|
474
|
+
}[];
|
|
475
|
+
};
|
|
476
|
+
/** One rate. `code` is unique within the method (e.g. a service level); `data` is kept with the order (e.g. a carrier rate id). */
|
|
477
|
+
export type RateQuote = {
|
|
478
|
+
code: string;
|
|
479
|
+
name: string;
|
|
480
|
+
amount: number;
|
|
481
|
+
carrier?: string | null;
|
|
482
|
+
minDeliveryDays?: number | null;
|
|
483
|
+
maxDeliveryDays?: number | null;
|
|
484
|
+
data?: Record<string, unknown>;
|
|
485
|
+
};
|
|
486
|
+
/** Stored with the order's shipping method, so fulfilment knows which provider, method and rate were chosen. */
|
|
487
|
+
export type ShippingRateDetails = {
|
|
488
|
+
provider: string;
|
|
489
|
+
method: string;
|
|
490
|
+
rate: string;
|
|
491
|
+
data: Record<string, unknown>;
|
|
492
|
+
};
|
|
493
|
+
export type ShipmentRequest = {
|
|
494
|
+
order: PluginOrder;
|
|
495
|
+
/** The units being shipped. */
|
|
496
|
+
lines: {
|
|
497
|
+
orderLineId: string;
|
|
498
|
+
sku: string;
|
|
499
|
+
name: string;
|
|
500
|
+
quantity: number;
|
|
501
|
+
weightGrams: number | null;
|
|
502
|
+
}[];
|
|
503
|
+
origin: Partial<Address> | null;
|
|
504
|
+
/** The method's provider options and the rate chosen at checkout, when the order used this provider. */
|
|
505
|
+
method: {
|
|
506
|
+
code: string;
|
|
507
|
+
options: Record<string, unknown>;
|
|
508
|
+
} | null;
|
|
509
|
+
rate: ShippingRateDetails | null;
|
|
510
|
+
};
|
|
511
|
+
export type ShipmentResult = {
|
|
512
|
+
tracking: TrackingNumber[];
|
|
513
|
+
labelUrl?: string | null;
|
|
514
|
+
cost?: {
|
|
515
|
+
amount: number;
|
|
516
|
+
currency: string;
|
|
517
|
+
} | null;
|
|
518
|
+
data?: Record<string, unknown>;
|
|
519
|
+
};
|
|
520
|
+
/**
|
|
521
|
+
* A carrier or rate engine. Shipping methods (Settings › Shipping) pick a provider and set its
|
|
522
|
+
* options. Live rates are cached per request for `cacheSeconds` (default 600) and never fetched
|
|
523
|
+
* while an order is being written, so slow carriers cannot hold up checkout.
|
|
524
|
+
*/
|
|
525
|
+
export interface ShippingProviderDefinition<S extends SettingsShape = SettingsShape> {
|
|
526
|
+
/** Unique across plugins, e.g. "shippo". */
|
|
527
|
+
readonly code: string;
|
|
528
|
+
readonly title: string;
|
|
529
|
+
readonly description?: string;
|
|
530
|
+
/** Options per shipping method, e.g. carriers or a markup. Secrets belong in plugin settings, not here. */
|
|
531
|
+
readonly methodOptions?: SettingsShape;
|
|
532
|
+
readonly cacheSeconds?: number;
|
|
533
|
+
/** Rates for a cart; [] when none apply. Limited to 10 seconds. */
|
|
534
|
+
rates(input: RateRequest, ctx: PluginContext<S>): Promise<RateQuote[]>;
|
|
535
|
+
/** Buy a label or book the shipment when staff fulfil (on request). */
|
|
536
|
+
createShipment?(input: ShipmentRequest, ctx: PluginContext<S>): Promise<ShipmentResult>;
|
|
537
|
+
trackingUrl?(tracking: {
|
|
538
|
+
carrier: string;
|
|
539
|
+
number: string;
|
|
540
|
+
}): string | null;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Run by the worker at most once per interval, on one instance at a time. On serverless hosts the
|
|
544
|
+
* worker runs on each cron call and with traffic, so the interval is a minimum, not a schedule.
|
|
545
|
+
*/
|
|
546
|
+
export type ScheduledTask<S extends SettingsShape = SettingsShape> = {
|
|
547
|
+
name: string;
|
|
548
|
+
description?: string;
|
|
549
|
+
intervalMinutes: number;
|
|
550
|
+
run(ctx: PluginContext<S>): Promise<void>;
|
|
551
|
+
};
|
|
552
|
+
export type RouteApi<S extends SettingsShape> = {
|
|
553
|
+
/** The plugin's context for a request; answers 503 for the plugin while it is disabled or not configured. */
|
|
554
|
+
context(c: HonoContext): Promise<PluginContext<S>>;
|
|
555
|
+
};
|
|
556
|
+
export type GraphQLExtensionApi<S extends SettingsShape> = {
|
|
557
|
+
/** The platform's Pothos builder: add types with `builder.objectType`, fields with `builder.queryField`, … */
|
|
558
|
+
builder: typeof Builder;
|
|
559
|
+
/** Sub-graphs for `subGraphs`: the admin API, the storefront API or both. */
|
|
560
|
+
ADMIN: SubGraph[];
|
|
561
|
+
STOREFRONT: SubGraph[];
|
|
562
|
+
BOTH: SubGraph[];
|
|
563
|
+
/** Object refs of platform types (OrderRef, ProductRef, CustomerRef, …), to add fields to them. */
|
|
564
|
+
refs: typeof Refs;
|
|
565
|
+
/** The plugin's context inside a resolver; fails with InvalidStateError while it is disabled or not configured. */
|
|
566
|
+
context(ctx: GraphQLContext): Promise<PluginContext<S>>;
|
|
567
|
+
/** Global ids: `gid://ecom/<Type>/<uuid>`. */
|
|
568
|
+
toGlobalId(type: string, id: string): string;
|
|
569
|
+
rawId(id: {
|
|
570
|
+
id: string;
|
|
571
|
+
} | string): string;
|
|
572
|
+
};
|
|
573
|
+
export type PluginDefinition<S extends SettingsShape = SettingsShape> = {
|
|
574
|
+
/** Unique: lowercase letters, digits and dashes. It namespaces settings, storage, routes and events. */
|
|
575
|
+
name: string;
|
|
576
|
+
/** The plugin's own version, shown in the admin. */
|
|
577
|
+
version: string;
|
|
578
|
+
/** The plugin API it was written for: PLUGIN_API_VERSION. */
|
|
579
|
+
apiVersion: number;
|
|
580
|
+
title: string;
|
|
581
|
+
description?: string;
|
|
582
|
+
settings?: S;
|
|
583
|
+
/** Permissions it introduces, e.g. `reviews:moderate`, assignable to roles and usable in its pages and fields. */
|
|
584
|
+
permissions?: {
|
|
585
|
+
code: string;
|
|
586
|
+
description: string;
|
|
587
|
+
}[];
|
|
588
|
+
/** Event types it emits, each `<name>.<something>`, so webhooks and other plugins can subscribe. */
|
|
589
|
+
emits?: string[];
|
|
590
|
+
/** Keys: an event type, `prefix.*` or `*`. */
|
|
591
|
+
events?: Record<string, EventHandler<S>>;
|
|
592
|
+
hooks?: PluginHooks<S>;
|
|
593
|
+
payments?: PaymentProviderDefinition<S>[];
|
|
594
|
+
shipping?: ShippingProviderDefinition<S>[];
|
|
595
|
+
/** Registered once per process, before the schema is built. */
|
|
596
|
+
graphql?: (api: GraphQLExtensionApi<S>) => void;
|
|
597
|
+
/** Mounted at /plugins/<name>/. */
|
|
598
|
+
routes?: (router: Hono, api: RouteApi<S>) => void;
|
|
599
|
+
tasks?: ScheduledTask<S>[];
|
|
600
|
+
/** A directory of `.sql` files, applied once each in name order after the platform's migrations: `new URL("./migrations/", import.meta.url)`. */
|
|
601
|
+
migrations?: URL;
|
|
602
|
+
/** The admin module (default export from `defineAdminPlugin`), bundled into the admin at build time: `new URL("./admin.tsx", import.meta.url)`. */
|
|
603
|
+
admin?: URL;
|
|
604
|
+
/** Once per process, when the platform starts and the plugin is enabled and configured. */
|
|
605
|
+
onStart?: (ctx: PluginContext<S>) => Promise<void> | void;
|
|
606
|
+
/**
|
|
607
|
+
* "Test connection" in the admin: check the settings against the remote service (e.g. that an
|
|
608
|
+
* API key works) and say what was found. Limited to 15 seconds.
|
|
609
|
+
*/
|
|
610
|
+
check?: (ctx: PluginContext<S>) => Promise<{
|
|
611
|
+
ok: boolean;
|
|
612
|
+
message: string;
|
|
613
|
+
}>;
|
|
614
|
+
};
|
|
615
|
+
/** Settings types flow into every handler: `ctx.settings.apiKey` is a string when the field is required. */
|
|
616
|
+
export declare const definePlugin: <const S extends SettingsShape = Record<never, SettingField>>(definition: PluginDefinition<S>) => PluginDefinition<S>;
|
|
617
|
+
/** Any plugin, whatever its settings. */
|
|
618
|
+
export type AnyPluginDefinition = PluginDefinition<any>;
|
|
619
|
+
export type EcomConfig = {
|
|
620
|
+
plugins: AnyPluginDefinition[];
|
|
621
|
+
};
|
|
622
|
+
/** ecom.config.ts: `export default defineConfig({ plugins: [stripe(), email()] })`. */
|
|
623
|
+
export declare const defineConfig: (config: EcomConfig) => EcomConfig;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// errors.ts
|
|
2
|
+
var DomainError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = new.target.name;
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var ValidationError = class extends DomainError {
|
|
11
|
+
fieldErrors;
|
|
12
|
+
constructor(message, fieldErrors = []) {
|
|
13
|
+
super("VALIDATION_FAILED", message);
|
|
14
|
+
this.fieldErrors = fieldErrors;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var NotFoundError = class extends DomainError {
|
|
18
|
+
resource;
|
|
19
|
+
lookup;
|
|
20
|
+
constructor(resource, lookup) {
|
|
21
|
+
super("NOT_FOUND", `${resource} ${lookup} was not found`);
|
|
22
|
+
this.resource = resource;
|
|
23
|
+
this.lookup = lookup;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var PaymentDeclinedError = class extends DomainError {
|
|
27
|
+
declineCode;
|
|
28
|
+
constructor(declineCode, message) {
|
|
29
|
+
super("PAYMENT_DECLINED", message);
|
|
30
|
+
this.declineCode = declineCode;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var InvalidStateError = class extends DomainError {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super("INVALID_STATE", message);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// index.ts
|
|
40
|
+
var PLUGIN_API_VERSION = 1;
|
|
41
|
+
var setting = {
|
|
42
|
+
string: (field) => ({ ...field, type: "string" }),
|
|
43
|
+
secret: (field) => ({ ...field, type: "secret" }),
|
|
44
|
+
boolean: (field) => ({ ...field, type: "boolean" }),
|
|
45
|
+
number: (field) => ({ ...field, type: "number" }),
|
|
46
|
+
select: (field) => ({ ...field, type: "select" })
|
|
47
|
+
};
|
|
48
|
+
var definePlugin = (definition) => definition;
|
|
49
|
+
var defineConfig = (config) => config;
|
|
50
|
+
export {
|
|
51
|
+
InvalidStateError,
|
|
52
|
+
NotFoundError,
|
|
53
|
+
PLUGIN_API_VERSION,
|
|
54
|
+
PaymentDeclinedError,
|
|
55
|
+
ValidationError,
|
|
56
|
+
defineConfig,
|
|
57
|
+
definePlugin,
|
|
58
|
+
setting
|
|
59
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public stand-ins for the platform types the plugin API mentions, used by the copy of
|
|
3
|
+
* @spindlecommerce/plugin-sdk published to npm (scripts/plugin-sdk-package.ts). In an installation the
|
|
4
|
+
* package is the platform's own SDK, typed with the real ones.
|
|
5
|
+
*
|
|
6
|
+
* Only `Database` and `Address` are covered by `apiVersion`; the rest is internal to the platform, so
|
|
7
|
+
* it is typed loosely here.
|
|
8
|
+
*/
|
|
9
|
+
import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
10
|
+
/** The platform's PostgreSQL database (Drizzle), for the plugin's own tables. */
|
|
11
|
+
export type Database = PgDatabase<PgQueryResultHKT>;
|
|
12
|
+
/** A postal address; `countryCode` is ISO 3166-1 alpha-2. The same as the platform's (checked by test/unit/plugin-sdk-package.test.ts). */
|
|
13
|
+
export type Address = {
|
|
14
|
+
firstName: string;
|
|
15
|
+
lastName: string;
|
|
16
|
+
company?: string | null;
|
|
17
|
+
line1: string;
|
|
18
|
+
line2?: string | null;
|
|
19
|
+
city: string;
|
|
20
|
+
region?: string | null;
|
|
21
|
+
regionCode?: string | null;
|
|
22
|
+
postalCode: string;
|
|
23
|
+
countryCode: string;
|
|
24
|
+
phone?: string | null;
|
|
25
|
+
};
|
|
26
|
+
/** `ctx.platform`: the rest of the platform, outside the plugin API. */
|
|
27
|
+
export type Platform = unknown;
|
|
28
|
+
/** What a GraphQL resolver receives as its context. */
|
|
29
|
+
export type GraphQLContext = unknown;
|
|
30
|
+
export type SubGraph = "storefront" | "admin";
|
|
31
|
+
/** The platform's Pothos schema builder. */
|
|
32
|
+
export declare const builder: any;
|
package/dist/refs.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Object refs of the platform's GraphQL types, to add fields to. Typed in an installation. */
|
|
2
|
+
export declare const MoneyRef: any;
|
|
3
|
+
export declare const AddressRef: any;
|
|
4
|
+
export declare const SessionRef: any;
|
|
5
|
+
export declare const ChannelRef: any;
|
|
6
|
+
export declare const AttributeDefinitionRef: any;
|
|
7
|
+
export declare const ProductTypeRef: any;
|
|
8
|
+
export declare const ProductRef: any;
|
|
9
|
+
export declare const ProductVariantRef: any;
|
|
10
|
+
export declare const MediaRef: any;
|
|
11
|
+
export declare const CategoryRef: any;
|
|
12
|
+
export declare const MediaAssetRef: any;
|
|
13
|
+
export declare const CartRef: any;
|
|
14
|
+
export declare const CartLineRef: any;
|
|
15
|
+
export declare const CustomerRef: any;
|
|
16
|
+
export declare const CustomerAddressRef: any;
|
|
17
|
+
export declare const CustomerGroupRef: any;
|
|
18
|
+
export declare const OrderRef: any;
|
|
19
|
+
export declare const OrderLineRef: any;
|
|
20
|
+
export declare const InvoiceRef: any;
|
|
21
|
+
export declare const FulfillmentRef: any;
|
|
22
|
+
export declare const RefundRef: any;
|
|
23
|
+
export declare const OrderEventRef: any;
|
|
24
|
+
export declare const PaymentTransactionRef: any;
|
|
25
|
+
export declare const PromotionRef: any;
|
|
26
|
+
export declare const CouponRef: any;
|
|
27
|
+
export declare const StockLocationRef: any;
|
|
28
|
+
export declare const StockRef: any;
|
|
29
|
+
export declare const StockLevelRef: any;
|
|
30
|
+
export declare const TaxClassRef: any;
|
|
31
|
+
export declare const TaxRateRef: any;
|
|
32
|
+
export declare const TaxRuleRef: any;
|
|
33
|
+
export declare const ShippingZoneRef: any;
|
|
34
|
+
export declare const ShippingMethodRef: any;
|
|
35
|
+
export declare const PaymentMethodRef: any;
|
|
36
|
+
export declare const PageRef: any;
|
|
37
|
+
export declare const PageVersionRef: any;
|
|
38
|
+
export declare const ComponentRef: any;
|
|
39
|
+
export declare const ReleaseRef: any;
|
|
40
|
+
export declare const FrontendRef: any;
|
|
41
|
+
export declare const StaffUserRef: any;
|
|
42
|
+
export declare const RoleRef: any;
|
|
43
|
+
export declare const ApiKeyRef: any;
|
|
44
|
+
export declare const WebhookSubscriptionRef: any;
|
|
45
|
+
export declare const WebhookDeliveryRef: any;
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spindlecommerce/plugin-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The plugin API for SpindleCommerce: payment and shipping providers, integrations, GraphQL fields, routes and scheduled tasks as plugins.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"spindlecommerce",
|
|
7
|
+
"ecommerce",
|
|
8
|
+
"plugin",
|
|
9
|
+
"sdk"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://spindlecommerce.com",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=24.12"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"drizzle-orm": "^0.45.4",
|
|
28
|
+
"hono": "^4.13.13",
|
|
29
|
+
"pino": "^10.4.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|