Skip to content

Commit 73ecbd2

Browse files
committed
adds the extracted @gadgetinc/core package
1 parent 1f933b1 commit 73ecbd2

22 files changed

+2965
-1
lines changed

packages/core/package.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "@gadgetinc/core",
3+
"version": "0.1.0",
4+
"files": [
5+
"README.md",
6+
"dist/**/*"
7+
],
8+
"license": "MIT",
9+
"repository": "github:gadget-inc/js-clients",
10+
"homepage": "https://github.com/gadget-inc/js-clients/tree/main/packages/core",
11+
"type": "module",
12+
"exports": {
13+
"./package.json": "./package.json",
14+
".": {
15+
"import": "./dist/esm/index.js",
16+
"require": "./dist/cjs/index.js",
17+
"default": "./dist/esm/index.js"
18+
}
19+
},
20+
"source": "src/index.ts",
21+
"main": "dist/cjs/index.js",
22+
"sideEffects": false,
23+
"scripts": {
24+
"typecheck:main": "tsc --noEmit",
25+
"typecheck": "tsc --noEmit",
26+
"clean": "rimraf dist/ *.tsbuildinfo **/*.tsbuildinfo",
27+
"prebuild": "mkdir -p dist/cjs dist/esm && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json && echo '{\"type\": \"module\"}' > dist/esm/package.json",
28+
"build": "pnpm clean && pnpm prebuild && tsc -b tsconfig.cjs.json tsconfig.esm.json",
29+
"prepublishOnly": "pnpm build",
30+
"prerelease": "gitpkg publish"
31+
},
32+
"dependencies": {
33+
"klona": "^2.0.6"
34+
},
35+
"peerDependencies": {
36+
"@urql/core": "*",
37+
"graphql": "*",
38+
"graphql-ws": "*"
39+
},
40+
"devDependencies": {
41+
"type-fest": "^3.13.1"
42+
}
43+
}

packages/core/src/AnyClient.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { AnyConnection } from "./AnyConnection.js";
2+
import type { GadgetTransaction } from "./GadgetTransaction.js";
3+
import type { AnyInternalModelManager } from "./InternalModelManager.js";
4+
5+
export const $modelRelationships = Symbol.for("gadget/modelRelationships");
6+
7+
export type InternalModelManagerNamespace = {
8+
// internal model managers can be maps of model names to model managers, subnamespaces, or utility functions
9+
[key: string]: AnyInternalModelManager | InternalModelManagerNamespace | ((...args: any[]) => any);
10+
};
11+
12+
/**
13+
* An instance of any Gadget app's API client object
14+
*/
15+
export interface AnyClient {
16+
connection: AnyConnection;
17+
query(graphQL: string, variables?: Record<string, any>): Promise<any>;
18+
mutate(graphQL: string, variables?: Record<string, any>): Promise<any>;
19+
transaction<T>(callback: (transaction: GadgetTransaction) => Promise<T>): Promise<T>;
20+
internal: InternalModelManagerNamespace;
21+
apiClientCoreVersion?: string;
22+
[$modelRelationships]?: { [modelName: string]: { [apiIdentifier: string]: { type: string; model: string } } };
23+
}
24+
25+
/**
26+
* Checks if the given object is an instance of any Gadget app's generated JS client object
27+
*/
28+
export const isGadgetClient = (client: any): client is AnyClient => {
29+
return client && "connection" in client && client.connection && "endpoint" in client.connection;
30+
};

packages/core/src/AnyConnection.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { Client, ClientOptions } from "@urql/core";
2+
import type { ClientOptions as SubscriptionClientOptions, createClient as createSubscriptionClient } from "graphql-ws";
3+
import type { AuthenticationModeOptions, Exchanges } from "./ClientOptions.js";
4+
import { GadgetTransaction } from "./GadgetTransaction.js";
5+
6+
export interface GadgetSubscriptionClientOptions extends Partial<SubscriptionClientOptions> {
7+
urlParams?: Record<string, string | null | undefined>;
8+
connectionAttempts?: number;
9+
connectionGlobalTimeoutMs?: number;
10+
}
11+
12+
/**
13+
* Represents the current strategy for authenticating with the Gadget platform.
14+
* For individual users in web browsers, we authenticate using a session token stored client side, like a cookie, but with cross domain support.
15+
* For server to server communication, or traceable access from the browser, we use pre shared secrets called API Keys
16+
* And when within the Gadget platform itself, we use a private secret token called an Internal Auth Token. Internal Auth Tokens are managed by Gadget and should never be used by external developers.
17+
**/
18+
export enum AuthenticationMode {
19+
BrowserSession = "browser-session",
20+
APIKey = "api-key",
21+
Internal = "internal",
22+
InternalAuthToken = "internal-auth-token",
23+
Anonymous = "anonymous",
24+
Custom = "custom",
25+
}
26+
27+
export interface GadgetConnectionOptions {
28+
endpoint: string;
29+
authenticationMode?: AuthenticationModeOptions;
30+
websocketsEndpoint?: string;
31+
subscriptionClientOptions?: GadgetSubscriptionClientOptions;
32+
websocketImplementation?: typeof globalThis.WebSocket;
33+
fetchImplementation?: typeof globalThis.fetch;
34+
environment?: string;
35+
requestPolicy?: ClientOptions["requestPolicy"];
36+
applicationId?: string;
37+
baseRouteURL?: string;
38+
exchanges?: Exchanges;
39+
createSubscriptionClient?: typeof createSubscriptionClient;
40+
}
41+
42+
export type TransactionRun<T> = (transaction: GadgetTransaction) => Promise<T>;
43+
44+
export interface AnyConnection {
45+
endpoint: string;
46+
authenticationMode: AuthenticationMode;
47+
createSubscriptionClient: typeof createSubscriptionClient;
48+
options: GadgetConnectionOptions;
49+
get currentClient(): Client;
50+
transaction: {
51+
<T>(options: GadgetSubscriptionClientOptions, run: TransactionRun<T>): Promise<T>;
52+
<T>(run: TransactionRun<T>): Promise<T>;
53+
};
54+
close(): void;
55+
fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
56+
}

packages/core/src/ClientOptions.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import type { Exchange } from "@urql/core";
2+
import type { GadgetSubscriptionClientOptions } from "./AnyConnection.js";
3+
4+
/** All the options for a Gadget client */
5+
export interface ClientOptions {
6+
/**
7+
* The HTTP GraphQL endpoint this connection should connect to
8+
**/
9+
endpoint?: string;
10+
/**
11+
* The authentication strategy for connecting to the upstream API
12+
**/
13+
authenticationMode?: AuthenticationModeOptions;
14+
/**
15+
* The Websockets GraphQL endpoint this connection should connect to for transactional processing
16+
**/
17+
websocketsEndpoint?: string;
18+
/**
19+
* Custom options to pass along to the WS clients when creating them
20+
**/
21+
subscriptionClientOptions?: GadgetSubscriptionClientOptions;
22+
/**
23+
* The `WebSocket` constructor to use for building websockets. Defaults to `globalThis.WebSocket`.
24+
**/
25+
websocketImplementation?: any;
26+
/**
27+
* The `fetch` function to use for making HTTP requests. Defaults to `globalThis.fetch`.
28+
**/
29+
fetchImplementation?: typeof fetch;
30+
/**
31+
* Which of the Gadget application's environments this connection should connect to
32+
**/
33+
environment?: string;
34+
/**
35+
* The ID of the application. Not required -- only used for emitting telemetry
36+
**/
37+
applicationId?: string;
38+
/**
39+
* The root URL of the app's public HTTP surface. Used for building fully-qualified URLs when `api.fetch` is called with relative paths.
40+
*
41+
* This only needs to be passed if you are overriding the `endpoint` parameter to something that can't be used for building fully-qualified URLs from relative imports.
42+
**/
43+
baseRouteURL?: string;
44+
/**
45+
* A list of exchanges to merge into the default exchanges used by the client.
46+
*/
47+
exchanges?: Exchanges;
48+
}
49+
50+
/** Options to configure a specific browser-based authentication mode */
51+
export interface BrowserSessionAuthenticationModeOptions {
52+
/**
53+
* The initial token to set for browser authentication.
54+
* This is useful when your session is initialized by some external authentication system, like OAuth.
55+
*/
56+
initialToken?: string;
57+
58+
/**
59+
* Configures how the authentication token is persisted. See `BrowserSessionStorageType`.
60+
*/
61+
storageType?: BrowserSessionStorageType;
62+
/**
63+
* The shop ID to set shop tenant. Useful for fetching shop-specific data.
64+
*/
65+
shopId?: string;
66+
}
67+
68+
/**
69+
* If using the `browserSession` authentication mode, sets how long the stored authentication information will last for for each user.
70+
*/
71+
export enum BrowserSessionStorageType {
72+
/**
73+
* `Durable` authentications ask the browser to keep the user's authentication information around for as long as it can, like the "Remember Me" button on a lot of webpages. Uses `window.localStorage` to store authentication tokens.
74+
*/
75+
Durable = "Durable",
76+
/**
77+
* `Session` authentications ask the browser to keep the user's authentication information around for a given browser tab, and then remove it when the tab is closed. Useful for high security scenarios where authenticated sessions are sensitive and should be forgotten quickly, or where the user's identity is temporary and only needs to last a short while. Uses `window.sessionStorage` to store authentication tokens.
78+
*/
79+
Session = "session",
80+
/**
81+
* `Temporary` authentications don't ask the browser to keep the user's authentication information around at all, such that refreshing the page will result in the user having no saved authentication state and likely being logged out. Useful for high security scenarios where authenticated sessions are sensitive and should be forgotten quickly.
82+
*/
83+
Temporary = "temporary",
84+
}
85+
86+
/** Describes how to authenticate an instance of the client with the Gadget platform */
87+
export interface AuthenticationModeOptions {
88+
// Use an API key to authenticate with Gadget.
89+
// Not strictly required, but without this the client might be useless depending on the app's permissions.
90+
apiKey?: string;
91+
92+
// Use a web browser's `localStorage` or `sessionStorage` to persist authentication information.
93+
// This allows the browser to have a persistent identity as the user navigates around and logs in and out.
94+
browserSession?: boolean | BrowserSessionAuthenticationModeOptions;
95+
96+
// Use no authentication at all, and get access only to the data that the Unauthenticated backend role has access to.
97+
anonymous?: true;
98+
99+
// @deprecated Use internal instead
100+
internalAuthToken?: string;
101+
102+
// @private Use an internal platform auth token for authentication
103+
// This is used to communicate within Gadget itself and shouldn't be used to connect to Gadget from other systems
104+
internal?: {
105+
authToken: string;
106+
actAsSession?: boolean;
107+
getSessionId?: () => Promise<string | undefined>;
108+
};
109+
110+
// @private Use a passed custom function for managing authentication. For some fancy integrations that the API client supports, like embedded Shopify apps, we use platform native features to authenticate with the Gadget backend.
111+
custom?: {
112+
processFetch(input: RequestInfo | URL, init: RequestInit): Promise<void>;
113+
processTransactionConnectionParams(params: Record<string, any>): Promise<void>;
114+
};
115+
}
116+
117+
export interface Exchanges {
118+
/**
119+
* Exchanges to add before all other exchanges.
120+
*/
121+
beforeAll?: Exchange[];
122+
/**
123+
* Exchanges to add before any async exchanges.
124+
*/
125+
beforeAsync?: Exchange[];
126+
/**
127+
* Exchanges to add after all other exchanges.
128+
*/
129+
afterAll?: Exchange[];
130+
}

packages/core/src/DataHydrator.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export const Hydrators = {
2+
DateTime(value: string) {
3+
return new Date(value);
4+
},
5+
};
6+
7+
export type Hydration = keyof typeof Hydrators;
8+
9+
/** Instructions for a client to turn raw transport types (like strings) into useful client side types (like Dates). Unstable and not intended for developer use. */
10+
export interface HydrationPlan {
11+
[key: string]: Hydration;
12+
}
13+
14+
/**
15+
* Utility for declaratively transforming object trees.
16+
* Useful for turning API date strings into real Date objects, etc.
17+
* Declarative so that the operations it peforms can be serialized.
18+
*/
19+
export class DataHydrator {
20+
constructor(readonly plan: HydrationPlan) {}
21+
22+
apply(source: Record<string, any> | Record<string, any>[]) {
23+
if (Array.isArray(source)) {
24+
return source.map((object) => this.hydrateObject(object));
25+
} else {
26+
return this.hydrateObject(source);
27+
}
28+
}
29+
30+
private hydrateObject(object: Record<string, any>) {
31+
const hydrated = { ...object };
32+
for (const [key, hydrator] of Object.entries(this.plan)) {
33+
const value = hydrated[key];
34+
if (value != null) {
35+
hydrated[key] = Hydrators[hydrator](value);
36+
}
37+
}
38+
return hydrated;
39+
}
40+
}

0 commit comments

Comments
 (0)