@bosunski/laravel-cloud-sdk 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # Laravel Cloud SDK (TypeScript)
2
+
3
+ TypeScript client for the Laravel Cloud API, providing convenient wrappers for applications, environments, deployments, domains, database clusters, and instances endpoints.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ pnpm install @bosunski/laravel-cloud-sdk
9
+ ```
10
+
11
+ ```ts
12
+ import { LaravelCloudClient } from '@bosunski/laravel-cloud-sdk';
13
+
14
+ const client = new LaravelCloudClient({ apiToken: process.env.LARAVEL_CLOUD_TOKEN! });
15
+ const apps = await client.applications.list();
16
+ await client.environments.start('env-123', { redeploy: true });
17
+ await client.domains.create('env-123', { name: 'example.com' });
18
+ const databases = await client.databases.list({ include: ['schemas'] });
19
+ await client.instances.create('env-123', {
20
+ name: 'web-1',
21
+ type: 'service',
22
+ size: 'flex.m-1vcpu-1gb',
23
+ scalingType: 'auto',
24
+ maxReplicas: 2,
25
+ minReplicas: 1,
26
+ usesScheduler: true,
27
+ });
28
+ ```
29
+
30
+ ### CDN Usage
31
+
32
+ The package is published as an ES module, so you can load it via a CDN like jsDelivr:
33
+
34
+ ```html
35
+ <script type="module">
36
+ import { LaravelCloudClient } from 'https://cdn.jsdelivr.net/npm/@bosunski/laravel-cloud-sdk/+esm';
37
+
38
+ const client = new LaravelCloudClient({ apiToken: 'token' });
39
+ const databases = await client.databases.list();
40
+ console.log(databases);
41
+ </script>
42
+ ```
43
+
44
+ ## Testing
45
+
46
+ This repository uses [Vitest](https://vitest.dev/) with mocked fetch handlers. Run the full suite with:
47
+
48
+ ```bash
49
+ pnpm test
50
+ ```
51
+
52
+ To inspect coverage output:
53
+
54
+ ```bash
55
+ pnpm test -- --coverage
56
+ ```
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import { QueryParams } from '../utils/query.js';
3
+ export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
4
+ export interface ClientOptions {
5
+ apiToken: string;
6
+ baseUrl?: string;
7
+ fetch?: typeof globalThis.fetch;
8
+ userAgent?: string;
9
+ }
10
+ export interface RequestConfig<TResponse> {
11
+ path: string;
12
+ method?: HttpMethod;
13
+ query?: QueryParams;
14
+ headers?: Record<string, string>;
15
+ json?: unknown;
16
+ formData?: FormData;
17
+ schema?: z.ZodType<TResponse>;
18
+ signal?: AbortSignal;
19
+ accept?: string;
20
+ }
21
+ export declare class HttpClient {
22
+ private readonly apiToken;
23
+ private readonly baseUrl;
24
+ private readonly fetchImpl;
25
+ private readonly userAgent?;
26
+ constructor(options: ClientOptions);
27
+ request<TResponse = unknown>(config: RequestConfig<TResponse>): Promise<TResponse>;
28
+ }
@@ -0,0 +1,79 @@
1
+ import { ApiError } from './errors.js';
2
+ import { buildSearchParams } from '../utils/query.js';
3
+ const DEFAULT_BASE_URL = 'https://cloud.laravel.com/api';
4
+ const defaultFetch = (input, init) => {
5
+ if (typeof fetch === 'function') {
6
+ return fetch(input, init);
7
+ }
8
+ throw new Error('Global fetch is not available. Provide a fetch implementation in ClientOptions.');
9
+ };
10
+ export class HttpClient {
11
+ apiToken;
12
+ baseUrl;
13
+ fetchImpl;
14
+ userAgent;
15
+ constructor(options) {
16
+ if (!options.apiToken) {
17
+ throw new Error('apiToken is required to initialize the Laravel Cloud client.');
18
+ }
19
+ this.apiToken = options.apiToken;
20
+ this.baseUrl = options.baseUrl?.replace(/\/$/, '') ?? DEFAULT_BASE_URL;
21
+ this.fetchImpl = options.fetch ?? defaultFetch;
22
+ this.userAgent = options.userAgent;
23
+ }
24
+ async request(config) {
25
+ const method = config.method ?? 'GET';
26
+ const search = buildSearchParams(config.query);
27
+ const url = `${this.baseUrl}${config.path}${search}`;
28
+ const headers = new Headers({
29
+ Authorization: `Bearer ${this.apiToken}`,
30
+ Accept: config.accept ?? 'application/vnd.api+json',
31
+ ...config.headers,
32
+ });
33
+ if (this.userAgent) {
34
+ headers.set('User-Agent', this.userAgent);
35
+ }
36
+ let body;
37
+ if (config.formData) {
38
+ body = config.formData;
39
+ }
40
+ else if (config.json !== undefined) {
41
+ headers.set('Content-Type', 'application/json');
42
+ body = JSON.stringify(config.json);
43
+ }
44
+ const response = await this.fetchImpl(url, {
45
+ method,
46
+ headers,
47
+ body,
48
+ signal: config.signal,
49
+ });
50
+ const contentType = response.headers.get('content-type') ?? '';
51
+ const isJson = contentType.includes('application/json');
52
+ let parsedBody;
53
+ if (response.status !== 204) {
54
+ if (isJson) {
55
+ parsedBody = await response.json();
56
+ }
57
+ else if (!response.bodyUsed) {
58
+ parsedBody = await response.text();
59
+ }
60
+ }
61
+ if (!response.ok) {
62
+ const headersRecord = {};
63
+ response.headers.forEach((value, key) => {
64
+ headersRecord[key] = value;
65
+ });
66
+ throw new ApiError({
67
+ status: response.status,
68
+ statusText: response.statusText,
69
+ request: { method, url },
70
+ body: parsedBody,
71
+ headers: headersRecord,
72
+ });
73
+ }
74
+ if (config.schema) {
75
+ return config.schema.parse(parsedBody);
76
+ }
77
+ return parsedBody;
78
+ }
79
+ }
@@ -0,0 +1,21 @@
1
+ export interface ApiErrorRequest {
2
+ method: string;
3
+ url: string;
4
+ }
5
+ export interface ApiErrorOptions {
6
+ status: number;
7
+ statusText: string;
8
+ request: ApiErrorRequest;
9
+ body?: unknown;
10
+ headers?: Record<string, string>;
11
+ message?: string;
12
+ }
13
+ export declare class ApiError extends Error {
14
+ readonly status: number;
15
+ readonly statusText: string;
16
+ readonly request: ApiErrorRequest;
17
+ readonly body?: unknown;
18
+ readonly headers?: Record<string, string>;
19
+ constructor(options: ApiErrorOptions);
20
+ }
21
+ export declare const isApiError: (error: unknown) => error is ApiError;
@@ -0,0 +1,17 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ statusText;
4
+ request;
5
+ body;
6
+ headers;
7
+ constructor(options) {
8
+ super(options.message ?? `Request failed with status ${options.status}`);
9
+ this.name = 'ApiError';
10
+ this.status = options.status;
11
+ this.statusText = options.statusText;
12
+ this.request = options.request;
13
+ this.body = options.body;
14
+ this.headers = options.headers;
15
+ }
16
+ }
17
+ export const isApiError = (error) => error instanceof ApiError;
@@ -0,0 +1,21 @@
1
+ import { HttpClient, ClientOptions } from './http/client.js';
2
+ import { ApiError, isApiError } from './http/errors.js';
3
+ import { ApplicationsResource, Application, ApplicationListResponse, ApplicationResponse, CreateApplicationPayload, ListApplicationsOptions, Region, UpdateApplicationPayload } from './resources/applications.js';
4
+ import { DeploymentsResource, Deployment, DeploymentListResponse, DeploymentResponse, ListDeploymentsOptions } from './resources/deployments.js';
5
+ import { EnvironmentsResource, Environment, EnvironmentListResponse, EnvironmentResponse, ListEnvironmentsOptions, UpdateEnvironmentPayload, CreateEnvironmentPayload } from './resources/environments.js';
6
+ import { DomainsResource, Domain, DomainListResponse, DomainResponse, ListDomainsOptions, CreateDomainPayload, UpdateDomainPayload } from './resources/domains.js';
7
+ import { DatabasesResource, Database, DatabaseListResponse, DatabaseResponse, ListDatabasesOptions, CreateDatabasePayload, DatabaseTypeDefinition, DatabaseTypeField } from './resources/databases.js';
8
+ import { InstancesResource, Instance, InstanceListResponse, InstanceResponse, ListInstancesOptions, CreateInstancePayload, UpdateInstancePayload, InstanceBackgroundProcess } from './resources/instances.js';
9
+ export declare class LaravelCloudClient {
10
+ readonly applications: ApplicationsResource;
11
+ readonly environments: EnvironmentsResource;
12
+ readonly deployments: DeploymentsResource;
13
+ readonly domains: DomainsResource;
14
+ readonly databases: DatabasesResource;
15
+ readonly instances: InstancesResource;
16
+ private readonly http;
17
+ constructor(options: ClientOptions);
18
+ }
19
+ export { HttpClient };
20
+ export type { ClientOptions, Application, ApplicationListResponse, ApplicationResponse, CreateApplicationPayload, ListApplicationsOptions, Region, UpdateApplicationPayload, Deployment, DeploymentListResponse, DeploymentResponse, ListDeploymentsOptions, Environment, EnvironmentListResponse, EnvironmentResponse, ListEnvironmentsOptions, UpdateEnvironmentPayload, CreateEnvironmentPayload, Domain, DomainListResponse, DomainResponse, ListDomainsOptions, CreateDomainPayload, UpdateDomainPayload, Database, DatabaseListResponse, DatabaseResponse, ListDatabasesOptions, CreateDatabasePayload, DatabaseTypeDefinition, DatabaseTypeField, Instance, InstanceListResponse, InstanceResponse, ListInstancesOptions, CreateInstancePayload, UpdateInstancePayload, InstanceBackgroundProcess, };
21
+ export { ApiError, isApiError };
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ import { HttpClient } from './http/client.js';
2
+ import { ApiError, isApiError } from './http/errors.js';
3
+ import { ApplicationsResource, } from './resources/applications.js';
4
+ import { DeploymentsResource, } from './resources/deployments.js';
5
+ import { EnvironmentsResource, } from './resources/environments.js';
6
+ import { DomainsResource, } from './resources/domains.js';
7
+ import { DatabasesResource, } from './resources/databases.js';
8
+ import { InstancesResource, } from './resources/instances.js';
9
+ export class LaravelCloudClient {
10
+ applications;
11
+ environments;
12
+ deployments;
13
+ domains;
14
+ databases;
15
+ instances;
16
+ http;
17
+ constructor(options) {
18
+ this.http = new HttpClient(options);
19
+ this.applications = new ApplicationsResource(this.http);
20
+ this.environments = new EnvironmentsResource(this.http);
21
+ this.deployments = new DeploymentsResource(this.http);
22
+ this.domains = new DomainsResource(this.http);
23
+ this.databases = new DatabasesResource(this.http);
24
+ this.instances = new InstancesResource(this.http);
25
+ }
26
+ }
27
+ export { HttpClient };
28
+ export { ApiError, isApiError };
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ import { HttpClient } from '../http/client.js';
3
+ import { JsonApiResponse, JsonApiResource } from '../types/jsonApi.js';
4
+ declare const regionSchema: z.ZodEnum<{
5
+ "us-east-2": "us-east-2";
6
+ "us-east-1": "us-east-1";
7
+ "eu-central-1": "eu-central-1";
8
+ "eu-west-1": "eu-west-1";
9
+ "eu-west-2": "eu-west-2";
10
+ "ap-southeast-1": "ap-southeast-1";
11
+ "ap-southeast-2": "ap-southeast-2";
12
+ "ca-central-1": "ca-central-1";
13
+ "me-central-1": "me-central-1";
14
+ }>;
15
+ export type Region = z.infer<typeof regionSchema>;
16
+ export interface ApplicationRepository {
17
+ fullName?: string;
18
+ defaultBranch?: string;
19
+ }
20
+ export interface Application {
21
+ id: string;
22
+ name: string;
23
+ slug: string;
24
+ region: string;
25
+ slackChannel?: string | null;
26
+ createdAt: string;
27
+ repository?: ApplicationRepository;
28
+ raw: JsonApiResource;
29
+ }
30
+ export interface ListApplicationsOptions {
31
+ include?: Array<'organization' | 'environments' | 'defaultEnvironment'>;
32
+ filter?: {
33
+ name?: string;
34
+ region?: string;
35
+ slug?: string;
36
+ };
37
+ page?: {
38
+ number?: number;
39
+ size?: number;
40
+ };
41
+ }
42
+ export interface CreateApplicationPayload {
43
+ repository: string;
44
+ name: string;
45
+ region: Region;
46
+ }
47
+ export interface UpdateApplicationPayload {
48
+ name?: string;
49
+ slug?: string;
50
+ defaultEnvironmentId?: string;
51
+ repository?: string;
52
+ slackChannel?: string | null;
53
+ }
54
+ export interface ApplicationListResponse extends JsonApiResponse<Application[]> {
55
+ }
56
+ export interface ApplicationResponse extends JsonApiResponse<Application> {
57
+ }
58
+ export declare class ApplicationsResource {
59
+ private readonly client;
60
+ constructor(client: HttpClient);
61
+ list(options?: ListApplicationsOptions): Promise<ApplicationListResponse>;
62
+ retrieve(applicationId: string, options?: Pick<ListApplicationsOptions, 'include'>): Promise<ApplicationResponse>;
63
+ create(payload: CreateApplicationPayload): Promise<ApplicationResponse>;
64
+ update(applicationId: string, payload: UpdateApplicationPayload): Promise<ApplicationResponse>;
65
+ }
66
+ export {};
@@ -0,0 +1,163 @@
1
+ import { z } from 'zod';
2
+ import { createJsonApiCollectionSchema, createJsonApiResponseSchema, JsonApiResourceSchema, } from '../types/jsonApi.js';
3
+ const regionSchema = z.enum([
4
+ 'us-east-2',
5
+ 'us-east-1',
6
+ 'eu-central-1',
7
+ 'eu-west-1',
8
+ 'eu-west-2',
9
+ 'ap-southeast-1',
10
+ 'ap-southeast-2',
11
+ 'ca-central-1',
12
+ 'me-central-1',
13
+ ]);
14
+ const repositorySchema = z
15
+ .object({
16
+ full_name: z.string(),
17
+ default_branch: z.string(),
18
+ })
19
+ .partial();
20
+ const applicationAttributesSchema = z
21
+ .object({
22
+ name: z.string(),
23
+ slug: z.string(),
24
+ region: z.string(),
25
+ slack_channel: z.string().nullable().optional(),
26
+ created_at: z.string(),
27
+ repository: repositorySchema.optional(),
28
+ })
29
+ .passthrough();
30
+ const applicationResourceSchema = JsonApiResourceSchema.extend({
31
+ type: z.literal('applications'),
32
+ attributes: applicationAttributesSchema,
33
+ });
34
+ const applicationResponseSchema = createJsonApiResponseSchema(applicationResourceSchema);
35
+ const applicationCollectionSchema = createJsonApiCollectionSchema(applicationResourceSchema);
36
+ const mapApplication = (resource) => {
37
+ const parsed = applicationResourceSchema.parse(resource);
38
+ const { attributes } = parsed;
39
+ return {
40
+ id: parsed.id,
41
+ name: attributes.name,
42
+ slug: attributes.slug,
43
+ region: attributes.region,
44
+ slackChannel: attributes.slack_channel ?? null,
45
+ createdAt: attributes.created_at,
46
+ repository: attributes.repository
47
+ ? {
48
+ fullName: attributes.repository.full_name,
49
+ defaultBranch: attributes.repository.default_branch,
50
+ }
51
+ : undefined,
52
+ raw: parsed,
53
+ };
54
+ };
55
+ const serializeApplicationList = (response) => ({
56
+ data: response.data.map(mapApplication),
57
+ included: response.included,
58
+ meta: response.meta,
59
+ links: response.links,
60
+ });
61
+ const serializeApplicationResponse = (response) => ({
62
+ data: mapApplication(response.data),
63
+ included: response.included,
64
+ meta: response.meta,
65
+ links: response.links,
66
+ });
67
+ export class ApplicationsResource {
68
+ client;
69
+ constructor(client) {
70
+ this.client = client;
71
+ }
72
+ async list(options) {
73
+ const query = {};
74
+ if (options?.include?.length) {
75
+ query.include = options.include.join(',');
76
+ }
77
+ if (options?.filter?.name) {
78
+ query['filter[name]'] = options.filter.name;
79
+ }
80
+ if (options?.filter?.region) {
81
+ query['filter[region]'] = options.filter.region;
82
+ }
83
+ if (options?.filter?.slug) {
84
+ query['filter[slug]'] = options.filter.slug;
85
+ }
86
+ if (options?.page?.number !== undefined) {
87
+ query['page[number]'] = options.page.number;
88
+ }
89
+ if (options?.page?.size !== undefined) {
90
+ query['page[size]'] = options.page.size;
91
+ }
92
+ const response = await this.client.request({
93
+ path: '/applications',
94
+ method: 'GET',
95
+ query,
96
+ schema: applicationCollectionSchema,
97
+ });
98
+ return serializeApplicationList(response);
99
+ }
100
+ async retrieve(applicationId, options) {
101
+ const query = options?.include?.length
102
+ ? { include: options.include.join(',') }
103
+ : undefined;
104
+ const response = await this.client.request({
105
+ path: `/applications/${applicationId}`,
106
+ method: 'GET',
107
+ query,
108
+ schema: applicationResponseSchema,
109
+ });
110
+ return serializeApplicationResponse(response);
111
+ }
112
+ async create(payload) {
113
+ const parsePayload = z
114
+ .object({
115
+ repository: z.string().min(1),
116
+ name: z.string().min(3).max(40),
117
+ region: regionSchema,
118
+ })
119
+ .parse(payload);
120
+ const response = await this.client.request({
121
+ path: '/applications',
122
+ method: 'POST',
123
+ json: parsePayload,
124
+ schema: applicationResponseSchema,
125
+ });
126
+ return serializeApplicationResponse(response);
127
+ }
128
+ async update(applicationId, payload) {
129
+ const parsedPayload = z
130
+ .object({
131
+ name: z.string().min(3).max(40).optional(),
132
+ slug: z.string().min(3).optional(),
133
+ defaultEnvironmentId: z.string().optional(),
134
+ repository: z.string().optional(),
135
+ slackChannel: z.string().nullable().optional(),
136
+ })
137
+ .strict()
138
+ .parse(payload);
139
+ const form = new FormData();
140
+ if (parsedPayload.name !== undefined) {
141
+ form.set('name', parsedPayload.name);
142
+ }
143
+ if (parsedPayload.slug !== undefined) {
144
+ form.set('slug', parsedPayload.slug);
145
+ }
146
+ if (parsedPayload.defaultEnvironmentId !== undefined) {
147
+ form.set('default_environment_id', parsedPayload.defaultEnvironmentId);
148
+ }
149
+ if (parsedPayload.repository !== undefined) {
150
+ form.set('repository', parsedPayload.repository);
151
+ }
152
+ if (parsedPayload.slackChannel !== undefined) {
153
+ form.set('slack_channel', parsedPayload.slackChannel ?? '');
154
+ }
155
+ const response = await this.client.request({
156
+ path: `/applications/${applicationId}`,
157
+ method: 'PATCH',
158
+ formData: form,
159
+ schema: applicationResponseSchema,
160
+ });
161
+ return serializeApplicationResponse(response);
162
+ }
163
+ }
@@ -0,0 +1,74 @@
1
+ import { HttpClient } from '../http/client.js';
2
+ import { JsonApiResponse, JsonApiResource } from '../types/jsonApi.js';
3
+ export interface DatabaseConnection {
4
+ hostname: string;
5
+ port: number;
6
+ protocol: string;
7
+ driver: string;
8
+ username: string;
9
+ password: string;
10
+ [key: string]: unknown;
11
+ }
12
+ export type DatabaseConfig = Record<string, unknown>;
13
+ export interface Database {
14
+ id: string;
15
+ name: string;
16
+ type: string;
17
+ status: string;
18
+ region: string;
19
+ createdAt: string;
20
+ config: DatabaseConfig;
21
+ connection: DatabaseConnection;
22
+ raw: JsonApiResource;
23
+ }
24
+ export interface ListDatabasesOptions {
25
+ include?: Array<'schemas'>;
26
+ filter?: {
27
+ type?: string;
28
+ region?: string;
29
+ status?: string;
30
+ };
31
+ page?: {
32
+ number?: number;
33
+ size?: number;
34
+ };
35
+ }
36
+ export interface CreateDatabasePayload {
37
+ type: string;
38
+ name: string;
39
+ region: string;
40
+ config: DatabaseConfig;
41
+ clusterId?: number | null;
42
+ }
43
+ export interface DatabaseTypeField {
44
+ name: string;
45
+ type: string;
46
+ required: boolean;
47
+ nullable?: boolean;
48
+ description?: string;
49
+ min?: number;
50
+ max?: number;
51
+ enum?: string[];
52
+ example?: string;
53
+ raw: Record<string, unknown>;
54
+ }
55
+ export interface DatabaseTypeDefinition {
56
+ type: string;
57
+ label: string;
58
+ regions: string[];
59
+ configSchema: DatabaseTypeField[];
60
+ raw: Record<string, unknown>;
61
+ }
62
+ export interface DatabaseListResponse extends JsonApiResponse<Database[]> {
63
+ }
64
+ export interface DatabaseResponse extends JsonApiResponse<Database> {
65
+ }
66
+ export declare class DatabasesResource {
67
+ private readonly client;
68
+ constructor(client: HttpClient);
69
+ list(options?: ListDatabasesOptions): Promise<DatabaseListResponse>;
70
+ create(payload: CreateDatabasePayload): Promise<DatabaseResponse>;
71
+ retrieve(databaseId: string, options?: Pick<ListDatabasesOptions, 'include'>): Promise<DatabaseResponse>;
72
+ delete(databaseId: string): Promise<void>;
73
+ listTypes(): Promise<DatabaseTypeDefinition[]>;
74
+ }