Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,8 @@
url = https://github.com/distilled-mirror/spec-mirror-modal.git
ignore = dirty
shallow = true
[submodule "packages/meilisearch/specs/spec-mirror-meilisearch"]
path = packages/meilisearch/specs/spec-mirror-meilisearch
url = https://github.com/distilled-mirror/spec-mirror-meilisearch.git
ignore = dirty
shallow = true
19,993 changes: 19,993 additions & 0 deletions packages/meilisearch/.generated-specs/meilisearch.json

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions packages/meilisearch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"name": "@distilled.cloud/meilisearch",
"version": "1.0.0-rc.8",
"repository": {
"type": "git",
"url": "https://github.com/alchemy-run/distilled",
"directory": "packages/meilisearch"
},
"license": "Apache-2.0",
"type": "module",
"sideEffects": false,
"files": [
"lib",
"src"
],
"exports": {
".": {
"types": "./lib/index.d.ts",
"bun": "./src/index.ts",
"worker": "./src/index.ts",
"default": "./lib/index.js"
},
"./Credentials": {
"types": "./lib/credentials.d.ts",
"bun": "./src/credentials.ts",
"worker": "./src/credentials.ts",
"default": "./lib/credentials.js"
},
"./Errors": {
"types": "./lib/errors.d.ts",
"bun": "./src/errors.ts",
"worker": "./src/errors.ts",
"default": "./lib/errors.js"
},
"./Traits": {
"types": "./lib/traits.d.ts",
"bun": "./src/traits.ts",
"worker": "./src/traits.ts",
"default": "./lib/traits.js"
},
"./Pagination": {
"types": "./lib/pagination.d.ts",
"bun": "./src/pagination.ts",
"worker": "./src/pagination.ts",
"default": "./lib/pagination.js"
},
"./Protocol": {
"types": "./lib/protocol.d.ts",
"bun": "./src/protocol.ts",
"worker": "./src/protocol.ts",
"default": "./lib/protocol.js"
},
"./Retry": {
"types": "./lib/retry.d.ts",
"bun": "./src/retry.ts",
"worker": "./src/retry.ts",
"default": "./lib/retry.js"
},
"./*": {
"types": "./lib/services/*.d.ts",
"bun": "./src/services/*.ts",
"worker": "./src/services/*.ts",
"default": "./lib/services/*.js"
}
},
"scripts": {
"build": "tsc -b",
"convert": "bun scripts/convert.ts",
"generate": "bun scripts/convert.ts && bun scripts/generate.ts",
"typecheck": "tsc -p tsconfig.json",
"specs:fetch": "git submodule update --force --init --depth=1 specs/spec-mirror-meilisearch",
"specs:update": "git submodule update --remote --force --depth=1 specs/spec-mirror-meilisearch"
},
"dependencies": {
"@distilled.cloud/core": "workspace:*"
},
"devDependencies": {
"@types/bun": "catalog:tooling",
"@types/node": "catalog:tooling",
"@effect/platform-bun": "catalog:effect"
},
"peerDependencies": {
"effect": "catalog:effect"
}
}
32 changes: 32 additions & 0 deletions packages/meilisearch/scripts/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bun
/**
* convert — turn the Meilisearch OpenAPI spec into a Smithy 2.0 JSON model.
*
* Input: specs/spec-mirror-meilisearch/specs/openapi.json (spec submodule)
* patches/*.patch.json (RFC-6902 patches to the OpenAPI document)
* Output: .generated-specs/meilisearch.json
*
* The OpenAPI→Smithy converter lives in
* `@distilled.cloud/core/codegen/openapi`; this script is Meilisearch's
* pipeline config. `scripts/generate.ts` compiles the model into src/services.
*/
import * as path from "node:path";
import { runOpenApiConvert } from "@distilled.cloud/core/codegen/openapi-cli";

await runOpenApiConvert({
root: path.resolve(import.meta.dir, ".."),
specs: [
{
name: "meilisearch",
specPath: "specs/spec-mirror-meilisearch/specs/openapi.json",
},
],
// OpenAPI-document patches (v0 layout: flat patches/*.patch.json). The
// smithy-model patch chain in generate.ts is disabled (`patchesDir: false`).
patchesDir: "patches",
options: {
namespace: "com.meilisearch.api",
serviceName: "Meilisearch",
skipDeprecated: true,
},
});
93 changes: 93 additions & 0 deletions packages/meilisearch/scripts/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env bun
/**
* generate — turn the Smithy JSON model in .generated-specs into the
* Meilisearch Effect SDK.
*
* Input: .generated-specs/meilisearch.json (written by scripts/convert.ts)
* Output: src/services/meilisearch.ts + src/services/index.ts
*
* The smithy→SDK compiler and CLI pipeline live in
* `@distilled.cloud/core/codegen`; this script is Meilisearch's provider spec.
* Meilisearch keeps the wire's camelCase member names on the TS surface, so
* no member renaming or wire dictionaries appear here.
*/
import type { SdkSpec } from "@distilled.cloud/core/codegen/generator";
import { runGeneratorCli } from "@distilled.cloud/core/codegen/cli";

const NULLABLE_TRAIT = "com.distilled.openapi#nullable";
const ERROR_MATCHERS_TRAIT = "com.distilled.openapi#errorMatchers";
const RAW_RESPONSE_TRAIT = "com.distilled.openapi#rawResponse";
const SENSITIVE_TRAIT = "smithy.api#sensitive";

/** Meilisearch's provider spec for the shared smithy→SDK compiler. */
const meilisearchSpec: SdkSpec = {
nullableTrait: NULLABLE_TRAIT,
errorMatchersTrait: ERROR_MATCHERS_TRAIT,

extraBindings: [
{
// Sole member of a synthesized wrapper for bare array/scalar response
// bodies; as the response's only member, the response IS the payload.
trait: RAW_RESPONSE_TRAIT,
binding: "rawResponse",
pipe: "T.RawResponse()",
rootPipe: "T.RawResponseRoot()",
},
],

// Sensitive strings (API keys): the schema member carries T.SensitiveValue;
// the REST protocol delivers Redacted values and accepts string | Redacted
// on input.
memberTraitPipes: {
[SENSITIVE_TRAIT]: "T.SensitiveValue",
},
memberTsType: (m) =>
SENSITIVE_TRAIT in m.traits
? `string | Redacted.Redacted<string>${m.nullable ? " | null" : ""}`
: undefined,

// Unions surface as TS type unions over an opaque schema — the REST
// protocol passes union content through verbatim (wire names ARE the TS
// names), so no runtime case discrimination is needed.
union: ({ name, caseTargets, tsRef }) => [
`export type ${name} = ${caseTargets.map(tsRef).join(" | ") || "unknown"};`,
`export const ${name} = /*@__PURE__*/ S.Unknown as any as S.Schema<${name}>;\n`,
],

// One pagination profile: Meilisearch's cursor mode (`from` in, `next`
// out), traversed by core's paginateCursor.
paginationProfiles: {
cursor: {
strategy: "paginateCursor",
itemsFallback: "results",
},
},

operationDecl: {
contextType: "MeilisearchOpContext",
commonErrorType: "MeilisearchOpError",
commonErrorClasses: ["UnknownMeilisearchError"],
protocol: "MeilisearchProtocol",
retry: "Retry.Retry",
},

sourceNote: ".generated-specs (specs/spec-mirror-meilisearch)",

// Sensitive member types reference Redacted; pull the import in when used.
postProcess: (code) =>
code.includes("Redacted.Redacted<")
? code.replace(
`import * as S from "@distilled.cloud/core/schema";\n`,
`import * as S from "@distilled.cloud/core/schema";\nimport * as Redacted from "effect/Redacted";\n`,
)
: code,
};

runGeneratorCli({
description: "Generate the Meilisearch Effect SDK from the Smithy model",
root: `${import.meta.dir}/..`,
// patches/ holds OpenAPI-document patches consumed by scripts/convert.ts;
// there is no smithy-model patch chain.
patchesDir: false,
spec: () => meilisearchSpec,
});
57 changes: 57 additions & 0 deletions packages/meilisearch/src/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Meilisearch credentials — hand-written.
*
* The `Credentials` service holds an *effect* that resolves the current
* credentials on every request (the protocol layer resolves it per request
* on the calling fiber). Auth is a bearer API key (master key or a key
* created via `/keys`).
*/
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Redacted from "effect/Redacted";
import { ConfigError } from "@distilled.cloud/core/errors";

export const DEFAULT_API_BASE_URL = "http://localhost:7700";

export interface Config {
readonly apiKey: Redacted.Redacted<string>;
readonly apiBaseUrl: string;
}

export class Credentials extends Context.Service<
Credentials,
Effect.Effect<Config>
>()("MeilisearchCredentials") {}

/** Layer from a plain API key + optional base URL. */
export const fromApiKey = (config: {
readonly apiKey: string;
readonly apiBaseUrl?: string;
}): Layer.Layer<Credentials> =>
Layer.succeed(
Credentials,
Effect.succeed({
apiKey: Redacted.make(config.apiKey),
apiBaseUrl: config.apiBaseUrl ?? DEFAULT_API_BASE_URL,
}),
);

/** Reads MEILISEARCH_API_KEY (required) and MEILISEARCH_API_BASE_URL (optional). */
export const CredentialsFromEnv: Layer.Layer<Credentials> = Layer.succeed(
Credentials,
Effect.gen(function* () {
const apiKey = process.env.MEILISEARCH_API_KEY;

if (!apiKey) {
return yield* new ConfigError({
message: "MEILISEARCH_API_KEY environment variable is required",
});
}

return {
apiKey: Redacted.make(apiKey),
apiBaseUrl: process.env.MEILISEARCH_API_BASE_URL ?? DEFAULT_API_BASE_URL,
};
}).pipe(Effect.orDie),
);
50 changes: 50 additions & 0 deletions packages/meilisearch/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Meilisearch-specific error types.
*
* Re-exports the common HTTP errors from core and adds the Meilisearch
* fallback errors. Note the generated service module additionally defines
* its own per-status matcher classes (BadRequest/NotFound/…) for the
* statuses each operation declares — those share `_tag`s with the core
* classes here, so `catchTag` works against either.
*/
export {
BadGateway,
BadRequest,
Conflict,
ConfigError,
Forbidden,
GatewayTimeout,
InternalServerError,
Locked,
NotFound,
ServiceUnavailable,
TooManyRequests,
Unauthorized,
UnprocessableEntity,
HTTP_STATUS_MAP,
DEFAULT_ERRORS,
API_ERRORS,
} from "@distilled.cloud/core/errors";
export type { DefaultErrors } from "@distilled.cloud/core/errors";

import * as Schema from "effect/Schema";
import * as Category from "@distilled.cloud/core/category";

/** Unknown Meilisearch error — returned when nothing else matches the failure. */
export class UnknownMeilisearchError extends Schema.TaggedError<UnknownMeilisearchError>()(
"UnknownMeilisearchError",
{
code: Schema.optional(Schema.String),
message: Schema.optional(Schema.String),
body: Schema.Unknown,
},
).pipe(Category.withServerError) {}

/** Schema parse error wrapper. */
export class MeilisearchParseError extends Schema.TaggedError<MeilisearchParseError>()(
"MeilisearchParseError",
{
body: Schema.Unknown,
cause: Schema.Unknown,
},
).pipe(Category.withParseError) {}
27 changes: 27 additions & 0 deletions packages/meilisearch/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @distilled.cloud/meilisearch — Meilisearch SDK for Effect.
*
* `./services` is generated by `scripts/generate.ts` from the Smithy model
* in `.generated-specs` (written by `scripts/convert.ts` from the OpenAPI
* spec). Everything else in this folder is hand-written.
*
* @example
* ```ts
* import * as Meilisearch from "@distilled.cloud/meilisearch";
*
* const indexes = yield* Meilisearch.Services.meilisearch.listIndexes({});
* ```
*/
export * from "./credentials.ts";
export * from "./errors.ts";
export * as T from "./traits.ts";
export {
MeilisearchProtocol,
type MeilisearchOpError,
type MeilisearchOpContext,
} from "./protocol.ts";
export { paginateCursor } from "./pagination.ts";
export * as Retry from "./retry.ts";
export * as Services from "./services/index.ts";
export * from "./services/meilisearch.ts";
export { BadRequest, NotFound } from "./services/meilisearch.ts";
8 changes: 8 additions & 0 deletions packages/meilisearch/src/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Meilisearch pagination — hand-written.
*
* Meilisearch's task/batch list operations are cursor-mode (`from` in,
* `next` out); generated operations pass core's {@link paginateCursor}
* strategy to `API.makePaginated`.
*/
export { paginateCursor } from "@distilled.cloud/core/pagination";
Loading
Loading