@shivaedev/platform 0.0.0 → 0.2.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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/better-auth/adapter.d.ts +11 -0
- package/dist/better-auth/adapter.d.ts.map +1 -0
- package/dist/better-auth/adapter.js +101 -0
- package/dist/better-auth/adapter.js.map +1 -0
- package/dist/better-auth/relation.d.ts +47 -0
- package/dist/better-auth/relation.d.ts.map +1 -0
- package/dist/better-auth/relation.js +111 -0
- package/dist/better-auth/relation.js.map +1 -0
- package/dist/better-auth.d.ts +2 -0
- package/dist/better-auth.d.ts.map +1 -0
- package/dist/better-auth.js +2 -0
- package/dist/better-auth.js.map +1 -0
- package/dist/runtime/make.d.ts +4 -0
- package/dist/runtime/make.d.ts.map +1 -0
- package/dist/runtime/make.js +59 -0
- package/dist/runtime/make.js.map +1 -0
- package/dist/runtime/types.d.ts +20 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +2 -0
- package/dist/runtime.js.map +1 -0
- package/dist/testing/types.d.ts +26 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/testing/vitest.d.ts +8 -0
- package/dist/testing/vitest.d.ts.map +1 -0
- package/dist/testing/vitest.js +30 -0
- package/dist/testing/vitest.js.map +1 -0
- package/dist/testing.d.ts +3 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/package.json +81 -3
- package/src/better-auth/adapter.ts +226 -0
- package/src/better-auth/relation.ts +199 -0
- package/src/better-auth.ts +4 -0
- package/src/runtime/make.ts +84 -0
- package/src/runtime/types.ts +34 -0
- package/src/runtime.ts +7 -0
- package/src/testing/types.ts +71 -0
- package/src/testing/vitest.ts +100 -0
- package/src/testing.ts +8 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type AnyDatabase, type DatabaseRequirement } from "@shivaedev/effect-prisma/testing";
|
|
2
|
+
import { type CallerOptions, type CallerResult } from "@shivaedev/effect-trpc/testing";
|
|
3
|
+
import { Effect, type Layer } from "effect";
|
|
4
|
+
import type { MakePlatformItOptions, PlatformHarness, PlatformIt } from "./types.js";
|
|
5
|
+
export declare const makePlatformIt: <const Database extends AnyDatabase>(database: Database) => <CreateCaller extends (...arguments_: never[]) => object, Provided, LayerError, Extension extends object = Record<never, never>>(options: MakePlatformItOptions<Database, CreateCaller, Provided, LayerError, Extension> & {
|
|
6
|
+
readonly layer: Layer.Layer<Provided | DatabaseRequirement<Database> | Effect.Services<Database>, LayerError>;
|
|
7
|
+
}) => PlatformIt<PlatformHarness<Database, CallerOptions<CreateCaller>, CallerResult<CreateCaller>, Extension>, Provided | DatabaseRequirement<Database> | Effect.Services<Database>>;
|
|
8
|
+
//# sourceMappingURL=vitest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../../src/testing/vitest.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,mBAAmB,EAGxB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,YAAY,EAGjB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,EACX,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,cAAc,GACzB,KAAK,CAAC,QAAQ,SAAS,WAAW,EAAE,UAAU,QAAQ,MAEtD,YAAY,SAAS,CAAC,GAAG,UAAU,EAAE,KAAK,EAAE,KAAK,MAAM,EACvD,QAAQ,EACR,UAAU,EACV,SAAS,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAE/C,SAAS,qBAAqB,CAC7B,QAAQ,EACR,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,SAAS,CACT,GAAG;IACH,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAC1B,QAAQ,GAAG,mBAAmB,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EACpE,UAAU,CACV,CAAC;CACF,KACC,UAAU,CACZ,eAAe,CACd,QAAQ,EACR,aAAa,CAAC,YAAY,CAAC,EAC3B,YAAY,CAAC,YAAY,CAAC,EAC1B,SAAS,CACT,EACD,QAAQ,GAAG,mBAAmB,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAoDpE,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { withTestTransaction, } from "@shivaedev/effect-prisma/testing";
|
|
2
|
+
import { makeTrpcHarnessIt, } from "@shivaedev/effect-trpc/testing";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
export const makePlatformIt = (database) => (options) => {
|
|
5
|
+
const trpcIt = makeTrpcHarnessIt({
|
|
6
|
+
adapter: options.adapter,
|
|
7
|
+
createCaller: options.createCaller,
|
|
8
|
+
layer: options.layer,
|
|
9
|
+
around: (effect) => withTestTransaction(database, effect),
|
|
10
|
+
makeHarness: (trpc, context) => Effect.gen(function* () {
|
|
11
|
+
const services = yield* Effect.context();
|
|
12
|
+
const databaseEffect = database;
|
|
13
|
+
const db = yield* databaseEffect;
|
|
14
|
+
const extension = options.extend
|
|
15
|
+
? yield* options.extend({ db, trpc }, context)
|
|
16
|
+
: {};
|
|
17
|
+
const promise = (evaluate) => Effect.tryPromise(() => options.adapter.runWithServices(services, evaluate));
|
|
18
|
+
return { ...extension, db, promise, trpc };
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
return new Proxy(trpcIt, {
|
|
22
|
+
get(target, property, receiver) {
|
|
23
|
+
if (property === "effectApp") {
|
|
24
|
+
return trpcIt.effectTRPC;
|
|
25
|
+
}
|
|
26
|
+
return Reflect.get(target, property, receiver);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=vitest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.js","sourceRoot":"","sources":["../../src/testing/vitest.ts"],"names":[],"mappings":"AACA,OAAO,EAIN,mBAAmB,GACnB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAIN,iBAAiB,GACjB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAC;AAO5C,MAAM,CAAC,MAAM,cAAc,GAC1B,CAAqC,QAAkB,EAAE,EAAE,CAC3D,CAMC,OAWC,EASA,EAAE;IASH,MAAM,MAAM,GAAG,iBAAiB,CAI9B;QACD,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC;QACzD,WAAW,EAAE,CACZ,IAA0C,EAC1C,OAAO,EACqC,EAAE,CAC9C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,EAAY,CAAC;YACnD,MAAM,cAAc,GAAG,QAItB,CAAC;YACF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC;YACjC,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM;gBAC/B,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC;gBAC9C,CAAC,CAAE,EAAgB,CAAC;YACrB,MAAM,OAAO,GAAG,CAAQ,QAA8B,EAAE,EAAE,CACzD,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,CACtB,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,CACnD,CAAC;YACH,OAAO,EAAE,GAAG,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAa,CAAC;QACvD,CAAC,CAA8C;KAChD,CAAC,CAAC;IAEH,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE;QACxB,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ;YAC7B,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC;gBAC9B,OAAO,MAAM,CAAC,UAAU,CAAC;YAC1B,CAAC;YACD,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAChD,CAAC;KACD,CAEiC,CAAC;AACpC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,GACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,90 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/platform",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Opinionated Effect application
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Opinionated Effect application infrastructure for ShivaeDev services",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ShivaeDev/platform.git",
|
|
10
|
+
"directory": "packages/platform"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/platform#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ShivaeDev/platform/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=24"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false,
|
|
6
20
|
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"CHANGELOG.md",
|
|
7
24
|
"README.md"
|
|
8
25
|
],
|
|
26
|
+
"exports": {
|
|
27
|
+
"./better-auth": {
|
|
28
|
+
"types": "./dist/better-auth.d.ts",
|
|
29
|
+
"source": "./src/better-auth.ts",
|
|
30
|
+
"import": "./dist/better-auth.js",
|
|
31
|
+
"default": "./dist/better-auth.js"
|
|
32
|
+
},
|
|
33
|
+
"./runtime": {
|
|
34
|
+
"types": "./dist/runtime.d.ts",
|
|
35
|
+
"source": "./src/runtime.ts",
|
|
36
|
+
"import": "./dist/runtime.js",
|
|
37
|
+
"default": "./dist/runtime.js"
|
|
38
|
+
},
|
|
39
|
+
"./testing": {
|
|
40
|
+
"types": "./dist/testing.d.ts",
|
|
41
|
+
"source": "./src/testing.ts",
|
|
42
|
+
"import": "./dist/testing.js",
|
|
43
|
+
"default": "./dist/testing.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
9
47
|
"publishConfig": {
|
|
10
|
-
"access": "public"
|
|
48
|
+
"access": "public",
|
|
49
|
+
"provenance": true
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@effect/vitest": "4.0.0-beta.102",
|
|
53
|
+
"@shivaedev/effect-prisma": "0.4.0",
|
|
54
|
+
"@shivaedev/effect-trpc": "0.3.0",
|
|
55
|
+
"@trpc/server": "11.18.0",
|
|
56
|
+
"better-auth": "1.6.25",
|
|
57
|
+
"effect": "4.0.0-beta.102"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"better-auth": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@effect/vitest": "4.0.0-beta.102",
|
|
66
|
+
"@prisma-next/adapter-postgres": "0.15.0",
|
|
67
|
+
"@prisma-next/contract": "0.15.0",
|
|
68
|
+
"@prisma-next/postgres": "0.15.0",
|
|
69
|
+
"@prisma-next/sql-contract": "0.15.0",
|
|
70
|
+
"@prisma-next/target-postgres": "0.15.0",
|
|
71
|
+
"@shivaedev/effect-prisma": "0.4.0",
|
|
72
|
+
"@shivaedev/effect-trpc": "0.3.0",
|
|
73
|
+
"@trpc/server": "11.18.0",
|
|
74
|
+
"@types/node": "24.10.1",
|
|
75
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
76
|
+
"better-auth": "1.6.25",
|
|
77
|
+
"effect": "4.0.0-beta.102",
|
|
78
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
79
|
+
"vitest": "4.1.9"
|
|
80
|
+
},
|
|
81
|
+
"scripts": {
|
|
82
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
|
|
83
|
+
"check": "biome check .",
|
|
84
|
+
"test": "vitest run",
|
|
85
|
+
"test:package": "node scripts/test-package.mjs",
|
|
86
|
+
"typecheck": "tsc --noEmit",
|
|
87
|
+
"typecheck:compat": "tsc6 --noEmit",
|
|
88
|
+
"ready": "pnpm check && pnpm typecheck && pnpm typecheck:compat && pnpm test && pnpm build && pnpm test:package"
|
|
11
89
|
}
|
|
12
90
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { AnyDatabase, DatabaseServiceOf } from "@shivaedev/effect-prisma";
|
|
2
|
+
import type { BetterAuthOptions, DBAdapterInstance, Where } from "better-auth";
|
|
3
|
+
import { type CustomAdapter, createAdapterFactory } from "better-auth/adapters";
|
|
4
|
+
import { Effect, Option } from "effect";
|
|
5
|
+
import type { PlatformRuntime } from "../runtime/types.js";
|
|
6
|
+
import { type DynamicRelation, refineRelation } from "./relation.js";
|
|
7
|
+
|
|
8
|
+
type CleanedWhere = Required<Where>;
|
|
9
|
+
type FindOneInput = Parameters<CustomAdapter["findOne"]>[0];
|
|
10
|
+
type FindManyInput = Parameters<CustomAdapter["findMany"]>[0];
|
|
11
|
+
type UpdateInput<Value> = {
|
|
12
|
+
readonly model: string;
|
|
13
|
+
readonly update: Value;
|
|
14
|
+
readonly where: CleanedWhere[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface EffectPrismaAdapterOptions {
|
|
18
|
+
readonly debugLogs?: boolean;
|
|
19
|
+
/** Map Better Auth model names to Prisma contract model names. */
|
|
20
|
+
readonly modelName?: (model: string) => string;
|
|
21
|
+
readonly usePlural?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DynamicDatabase<Requirements> {
|
|
25
|
+
readonly transaction: <A, E, R>(
|
|
26
|
+
program: Effect.Effect<A, E, R>,
|
|
27
|
+
) => Effect.Effect<A, E | unknown, R | Requirements>;
|
|
28
|
+
readonly [model: string]: DynamicRelation<Requirements> | unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const modelRelation = <Requirements>(
|
|
32
|
+
database: DynamicDatabase<Requirements>,
|
|
33
|
+
model: string,
|
|
34
|
+
mapModelName: (model: string) => string,
|
|
35
|
+
): DynamicRelation<Requirements> =>
|
|
36
|
+
Reflect.get(database, mapModelName(model)) as DynamicRelation<Requirements>;
|
|
37
|
+
|
|
38
|
+
const defaultModelName = (model: string): string =>
|
|
39
|
+
model.length === 0 ? model : `${model[0]?.toUpperCase()}${model.slice(1)}`;
|
|
40
|
+
|
|
41
|
+
const rejectJoin = (join: unknown): void => {
|
|
42
|
+
if (join !== undefined) {
|
|
43
|
+
throw new TypeError(
|
|
44
|
+
"The Effect Prisma Better Auth adapter does not support experimental native joins",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const effectPrismaAdapter =
|
|
50
|
+
<Database extends AnyDatabase, Services, BuildError>(
|
|
51
|
+
databaseTag: Database,
|
|
52
|
+
runtime: PlatformRuntime<Services, BuildError>,
|
|
53
|
+
adapterOptions: EffectPrismaAdapterOptions = {},
|
|
54
|
+
): DBAdapterInstance =>
|
|
55
|
+
(authOptions: BetterAuthOptions) => {
|
|
56
|
+
const mapModelName = adapterOptions.modelName ?? defaultModelName;
|
|
57
|
+
const databaseEffect = databaseTag as Effect.Effect<
|
|
58
|
+
DatabaseServiceOf<Database>,
|
|
59
|
+
never,
|
|
60
|
+
Services
|
|
61
|
+
>;
|
|
62
|
+
|
|
63
|
+
const run = <Value>(
|
|
64
|
+
operation: (
|
|
65
|
+
database: DynamicDatabase<Services>,
|
|
66
|
+
) => Effect.Effect<Value, unknown, Services>,
|
|
67
|
+
): Promise<Value> =>
|
|
68
|
+
runtime.runPromise(
|
|
69
|
+
Effect.flatMap(databaseEffect, (database) =>
|
|
70
|
+
operation(database as DynamicDatabase<Services>),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
let factory: ReturnType<typeof createAdapterFactory>;
|
|
75
|
+
factory = createAdapterFactory({
|
|
76
|
+
config: {
|
|
77
|
+
adapterId: "effect-prisma",
|
|
78
|
+
adapterName: "Effect Prisma",
|
|
79
|
+
debugLogs: adapterOptions.debugLogs ?? false,
|
|
80
|
+
supportsArrays: true,
|
|
81
|
+
supportsBooleans: true,
|
|
82
|
+
supportsDates: true,
|
|
83
|
+
supportsJSON: true,
|
|
84
|
+
supportsNumericIds: true,
|
|
85
|
+
supportsUUIDs: true,
|
|
86
|
+
transaction: (callback) =>
|
|
87
|
+
run((database) =>
|
|
88
|
+
database.transaction(
|
|
89
|
+
Effect.flatMap(Effect.context<Services>(), (services) =>
|
|
90
|
+
Effect.tryPromise({
|
|
91
|
+
try: () =>
|
|
92
|
+
runtime.runWithServices(services, () =>
|
|
93
|
+
callback(factory(authOptions)),
|
|
94
|
+
),
|
|
95
|
+
catch: (error) => error,
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
adapter: ({ debugLog, getFieldAttributes }) => ({
|
|
102
|
+
create: async ({ data, model, select }) => {
|
|
103
|
+
debugLog("create", { model });
|
|
104
|
+
return (await run((database) =>
|
|
105
|
+
refineRelation(modelRelation(database, model, mapModelName), {
|
|
106
|
+
select,
|
|
107
|
+
}).create(data),
|
|
108
|
+
)) as typeof data;
|
|
109
|
+
},
|
|
110
|
+
findOne: async <Value>({
|
|
111
|
+
join,
|
|
112
|
+
model,
|
|
113
|
+
select,
|
|
114
|
+
where,
|
|
115
|
+
}: FindOneInput) => {
|
|
116
|
+
rejectJoin(join);
|
|
117
|
+
return (await run((database) =>
|
|
118
|
+
Effect.map(
|
|
119
|
+
refineRelation(modelRelation(database, model, mapModelName), {
|
|
120
|
+
select,
|
|
121
|
+
where,
|
|
122
|
+
}).first(),
|
|
123
|
+
Option.getOrNull,
|
|
124
|
+
),
|
|
125
|
+
)) as Value | null;
|
|
126
|
+
},
|
|
127
|
+
findMany: async <Value>({
|
|
128
|
+
join,
|
|
129
|
+
limit,
|
|
130
|
+
model,
|
|
131
|
+
offset,
|
|
132
|
+
select,
|
|
133
|
+
sortBy,
|
|
134
|
+
where,
|
|
135
|
+
}: FindManyInput) => {
|
|
136
|
+
rejectJoin(join);
|
|
137
|
+
return (await run(
|
|
138
|
+
(database) =>
|
|
139
|
+
refineRelation(modelRelation(database, model, mapModelName), {
|
|
140
|
+
limit,
|
|
141
|
+
offset,
|
|
142
|
+
select,
|
|
143
|
+
sortBy,
|
|
144
|
+
where,
|
|
145
|
+
}) as unknown as Effect.Effect<
|
|
146
|
+
ReadonlyArray<Record<string, unknown>>,
|
|
147
|
+
unknown,
|
|
148
|
+
Services
|
|
149
|
+
>,
|
|
150
|
+
)) as Value[];
|
|
151
|
+
},
|
|
152
|
+
update: async <Value>({ model, update, where }: UpdateInput<Value>) => {
|
|
153
|
+
if (where.length === 0) return null;
|
|
154
|
+
const hasUniqueCondition = where.some(
|
|
155
|
+
(condition) =>
|
|
156
|
+
condition.connector !== "OR" &&
|
|
157
|
+
condition.operator === "eq" &&
|
|
158
|
+
condition.mode !== "insensitive" &&
|
|
159
|
+
(condition.field === "id" ||
|
|
160
|
+
getFieldAttributes({
|
|
161
|
+
field: condition.field,
|
|
162
|
+
model,
|
|
163
|
+
}).unique === true),
|
|
164
|
+
);
|
|
165
|
+
return (await run((database) => {
|
|
166
|
+
const relation = refineRelation(
|
|
167
|
+
modelRelation(database, model, mapModelName),
|
|
168
|
+
{ where },
|
|
169
|
+
);
|
|
170
|
+
return hasUniqueCondition
|
|
171
|
+
? relation.update(update as Record<string, unknown>)
|
|
172
|
+
: Effect.map(
|
|
173
|
+
relation.updateAll(update as Record<string, unknown>),
|
|
174
|
+
(rows) => rows[0] ?? null,
|
|
175
|
+
);
|
|
176
|
+
})) as Value | null;
|
|
177
|
+
},
|
|
178
|
+
updateMany: ({ model, update, where }) =>
|
|
179
|
+
run((database) =>
|
|
180
|
+
Effect.map(
|
|
181
|
+
refineRelation(modelRelation(database, model, mapModelName), {
|
|
182
|
+
where,
|
|
183
|
+
}).updateAll(update),
|
|
184
|
+
(rows) => rows.length,
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
delete: ({ model, where }) => {
|
|
188
|
+
const hasId = where.some(
|
|
189
|
+
(condition) =>
|
|
190
|
+
condition.connector !== "OR" &&
|
|
191
|
+
condition.field === "id" &&
|
|
192
|
+
condition.operator === "eq",
|
|
193
|
+
);
|
|
194
|
+
return run((database) => {
|
|
195
|
+
const relation = refineRelation(
|
|
196
|
+
modelRelation(database, model, mapModelName),
|
|
197
|
+
{ where },
|
|
198
|
+
);
|
|
199
|
+
return hasId
|
|
200
|
+
? Effect.asVoid(relation.delete())
|
|
201
|
+
: Effect.asVoid(relation.deleteAll());
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
deleteMany: ({ model, where }) =>
|
|
205
|
+
run((database) =>
|
|
206
|
+
Effect.map(
|
|
207
|
+
refineRelation(modelRelation(database, model, mapModelName), {
|
|
208
|
+
where,
|
|
209
|
+
}).deleteAll(),
|
|
210
|
+
(rows) => rows.length,
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
count: ({ model, where }) =>
|
|
214
|
+
run((database) =>
|
|
215
|
+
refineRelation(modelRelation(database, model, mapModelName), {
|
|
216
|
+
where: where as CleanedWhere[] | undefined,
|
|
217
|
+
}).count(),
|
|
218
|
+
),
|
|
219
|
+
options: {
|
|
220
|
+
usePlural: adapterOptions.usePlural ?? false,
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return factory(authOptions);
|
|
226
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { all, and, not, or } from "@shivaedev/effect-prisma";
|
|
2
|
+
import type { Where } from "better-auth";
|
|
3
|
+
import type { Effect, Option } from "effect";
|
|
4
|
+
|
|
5
|
+
type CleanedWhere = Required<Where>;
|
|
6
|
+
type Expression = Parameters<typeof and>[number];
|
|
7
|
+
|
|
8
|
+
interface DynamicField {
|
|
9
|
+
eq(value: unknown): Expression;
|
|
10
|
+
neq(value: unknown): Expression;
|
|
11
|
+
gt(value: unknown): Expression;
|
|
12
|
+
gte(value: unknown): Expression;
|
|
13
|
+
lt(value: unknown): Expression;
|
|
14
|
+
lte(value: unknown): Expression;
|
|
15
|
+
in(value: ReadonlyArray<unknown>): Expression;
|
|
16
|
+
notIn(value: ReadonlyArray<unknown>): Expression;
|
|
17
|
+
like(value: string): Expression;
|
|
18
|
+
ilike?(value: string): Expression;
|
|
19
|
+
isNull(): Expression;
|
|
20
|
+
isNotNull(): Expression;
|
|
21
|
+
asc(): unknown;
|
|
22
|
+
desc(): unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DynamicRelation<Requirements> {
|
|
26
|
+
where(
|
|
27
|
+
predicate: (fields: Record<string, DynamicField>) => Expression,
|
|
28
|
+
): DynamicRelation<Requirements>;
|
|
29
|
+
orderBy(
|
|
30
|
+
ordering: (fields: Record<string, DynamicField>) => unknown,
|
|
31
|
+
): DynamicRelation<Requirements>;
|
|
32
|
+
take(count: number): DynamicRelation<Requirements>;
|
|
33
|
+
skip(count: number): DynamicRelation<Requirements>;
|
|
34
|
+
select(...fields: ReadonlyArray<string>): DynamicRelation<Requirements>;
|
|
35
|
+
first(): Effect.Effect<
|
|
36
|
+
Option.Option<Record<string, unknown>>,
|
|
37
|
+
unknown,
|
|
38
|
+
Requirements
|
|
39
|
+
>;
|
|
40
|
+
count(): Effect.Effect<number, unknown, Requirements>;
|
|
41
|
+
create(
|
|
42
|
+
data: Record<string, unknown>,
|
|
43
|
+
): Effect.Effect<Record<string, unknown>, unknown, Requirements>;
|
|
44
|
+
update(
|
|
45
|
+
data: Record<string, unknown>,
|
|
46
|
+
): Effect.Effect<Record<string, unknown> | null, unknown, Requirements>;
|
|
47
|
+
updateAll(
|
|
48
|
+
data: Record<string, unknown>,
|
|
49
|
+
): Effect.Effect<
|
|
50
|
+
ReadonlyArray<Record<string, unknown>>,
|
|
51
|
+
unknown,
|
|
52
|
+
Requirements
|
|
53
|
+
>;
|
|
54
|
+
delete(): Effect.Effect<
|
|
55
|
+
Record<string, unknown> | null,
|
|
56
|
+
unknown,
|
|
57
|
+
Requirements
|
|
58
|
+
>;
|
|
59
|
+
deleteAll(): Effect.Effect<
|
|
60
|
+
ReadonlyArray<Record<string, unknown>>,
|
|
61
|
+
unknown,
|
|
62
|
+
Requirements
|
|
63
|
+
>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const escapeLike = (value: string): string =>
|
|
67
|
+
value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
68
|
+
|
|
69
|
+
const comparison = (
|
|
70
|
+
field: DynamicField,
|
|
71
|
+
where: CleanedWhere,
|
|
72
|
+
): Expression | undefined => {
|
|
73
|
+
const value = where.value;
|
|
74
|
+
if (
|
|
75
|
+
where.mode === "insensitive" &&
|
|
76
|
+
typeof value === "string" &&
|
|
77
|
+
field.ilike !== undefined
|
|
78
|
+
) {
|
|
79
|
+
if (where.operator === "eq") return field.ilike(escapeLike(value));
|
|
80
|
+
if (where.operator === "ne") return not(field.ilike(escapeLike(value)));
|
|
81
|
+
}
|
|
82
|
+
switch (where.operator) {
|
|
83
|
+
case "eq":
|
|
84
|
+
return value === null ? field.isNull() : field.eq(value);
|
|
85
|
+
case "ne":
|
|
86
|
+
return value === null ? field.isNotNull() : field.neq(value);
|
|
87
|
+
case "lt":
|
|
88
|
+
return field.lt(value);
|
|
89
|
+
case "lte":
|
|
90
|
+
return field.lte(value);
|
|
91
|
+
case "gt":
|
|
92
|
+
return field.gt(value);
|
|
93
|
+
case "gte":
|
|
94
|
+
return field.gte(value);
|
|
95
|
+
case "in": {
|
|
96
|
+
const values = (Array.isArray(value) ? value : [value]).filter(
|
|
97
|
+
(item) => item !== null,
|
|
98
|
+
);
|
|
99
|
+
const ilike = field.ilike;
|
|
100
|
+
return values.length === 0
|
|
101
|
+
? and(field.isNull(), field.isNotNull())
|
|
102
|
+
: where.mode === "insensitive" &&
|
|
103
|
+
ilike !== undefined &&
|
|
104
|
+
values.every((item) => typeof item === "string")
|
|
105
|
+
? or(...values.map((item) => ilike(escapeLike(String(item)))))
|
|
106
|
+
: field.in(values);
|
|
107
|
+
}
|
|
108
|
+
case "not_in": {
|
|
109
|
+
const values = (Array.isArray(value) ? value : [value]).filter(
|
|
110
|
+
(item) => item !== null,
|
|
111
|
+
);
|
|
112
|
+
if (values.length === 0) return undefined;
|
|
113
|
+
const ilike = field.ilike;
|
|
114
|
+
return where.mode === "insensitive" &&
|
|
115
|
+
ilike !== undefined &&
|
|
116
|
+
values.every((item) => typeof item === "string")
|
|
117
|
+
? and(...values.map((item) => not(ilike(escapeLike(String(item))))))
|
|
118
|
+
: field.notIn(values);
|
|
119
|
+
}
|
|
120
|
+
case "contains":
|
|
121
|
+
case "starts_with":
|
|
122
|
+
case "ends_with": {
|
|
123
|
+
if (typeof value !== "string") {
|
|
124
|
+
throw new TypeError(`${where.operator} requires a string value`);
|
|
125
|
+
}
|
|
126
|
+
const escaped = escapeLike(value);
|
|
127
|
+
const pattern =
|
|
128
|
+
where.operator === "contains"
|
|
129
|
+
? `%${escaped}%`
|
|
130
|
+
: where.operator === "starts_with"
|
|
131
|
+
? `${escaped}%`
|
|
132
|
+
: `%${escaped}`;
|
|
133
|
+
return where.mode === "insensitive" && field.ilike !== undefined
|
|
134
|
+
? field.ilike(pattern)
|
|
135
|
+
: field.like(pattern);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const whereExpression = (
|
|
141
|
+
fields: Record<string, DynamicField>,
|
|
142
|
+
where: ReadonlyArray<CleanedWhere>,
|
|
143
|
+
): Expression | undefined => {
|
|
144
|
+
const conjunctions: Expression[] = [];
|
|
145
|
+
const disjunctions: Expression[] = [];
|
|
146
|
+
|
|
147
|
+
for (const condition of where) {
|
|
148
|
+
const field = fields[condition.field];
|
|
149
|
+
if (field === undefined) {
|
|
150
|
+
throw new TypeError(`Unknown database field: ${condition.field}`);
|
|
151
|
+
}
|
|
152
|
+
const expression = comparison(field, condition);
|
|
153
|
+
if (expression === undefined) continue;
|
|
154
|
+
(condition.connector === "OR" ? disjunctions : conjunctions).push(
|
|
155
|
+
expression,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (disjunctions.length > 0) conjunctions.push(or(...disjunctions));
|
|
160
|
+
if (conjunctions.length === 0) return all();
|
|
161
|
+
return conjunctions.length === 1 ? conjunctions[0] : and(...conjunctions);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const refineRelation = <Requirements>(
|
|
165
|
+
relation: DynamicRelation<Requirements>,
|
|
166
|
+
options: {
|
|
167
|
+
readonly where?: ReadonlyArray<CleanedWhere>;
|
|
168
|
+
readonly select?: ReadonlyArray<string>;
|
|
169
|
+
readonly limit?: number;
|
|
170
|
+
readonly offset?: number;
|
|
171
|
+
readonly sortBy?: {
|
|
172
|
+
readonly direction: "asc" | "desc";
|
|
173
|
+
readonly field: string;
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
): DynamicRelation<Requirements> => {
|
|
177
|
+
let refined = relation;
|
|
178
|
+
if (options.where !== undefined) {
|
|
179
|
+
refined = refined.where((fields) => {
|
|
180
|
+
const expression = whereExpression(fields, options.where ?? []);
|
|
181
|
+
return expression ?? all();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (options.sortBy !== undefined) {
|
|
185
|
+
refined = refined.orderBy((fields) => {
|
|
186
|
+
const field = fields[options.sortBy?.field ?? ""];
|
|
187
|
+
if (field === undefined) {
|
|
188
|
+
throw new TypeError(`Unknown database field: ${options.sortBy?.field}`);
|
|
189
|
+
}
|
|
190
|
+
return options.sortBy?.direction === "desc" ? field.desc() : field.asc();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (options.offset !== undefined) refined = refined.skip(options.offset);
|
|
194
|
+
if (options.limit !== undefined) refined = refined.take(options.limit);
|
|
195
|
+
if (options.select !== undefined && options.select.length > 0) {
|
|
196
|
+
refined = refined.select(...options.select);
|
|
197
|
+
}
|
|
198
|
+
return refined;
|
|
199
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { Layer } from "effect";
|
|
3
|
+
import { Cause, Context, Effect, Exit, ManagedRuntime } from "effect";
|
|
4
|
+
import type { PlatformRuntime, PlatformRuntimeOptions } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const RuntimeCacheKey = Symbol.for("@shivaedev/platform/runtime-cache");
|
|
7
|
+
const globalRuntimeCache = globalThis as typeof globalThis & {
|
|
8
|
+
[RuntimeCacheKey]?: Map<string | symbol, PlatformRuntime<unknown, unknown>>;
|
|
9
|
+
};
|
|
10
|
+
const runtimeCache =
|
|
11
|
+
globalRuntimeCache[RuntimeCacheKey] ??
|
|
12
|
+
new Map<string | symbol, PlatformRuntime<unknown, unknown>>();
|
|
13
|
+
globalRuntimeCache[RuntimeCacheKey] = runtimeCache;
|
|
14
|
+
|
|
15
|
+
const make = <Services, BuildError>(
|
|
16
|
+
layer: Layer.Layer<Services, BuildError>,
|
|
17
|
+
): PlatformRuntime<Services, BuildError> => {
|
|
18
|
+
const managed = ManagedRuntime.make(layer);
|
|
19
|
+
const services = new AsyncLocalStorage<Context.Context<never>>();
|
|
20
|
+
|
|
21
|
+
const currentServices = () => services.getStore();
|
|
22
|
+
const withAmbient = <A, E, Requirements extends Services>(
|
|
23
|
+
effect: Effect.Effect<A, E, Requirements>,
|
|
24
|
+
): Effect.Effect<A, E | BuildError, Requirements> => {
|
|
25
|
+
const ambient = currentServices();
|
|
26
|
+
return ambient === undefined
|
|
27
|
+
? effect
|
|
28
|
+
: Effect.flatMap(managed.contextEffect, (base) =>
|
|
29
|
+
Effect.provideContext(effect, Context.merge(base, ambient)),
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const runPromiseExit: PlatformRuntime<
|
|
34
|
+
Services,
|
|
35
|
+
BuildError
|
|
36
|
+
>["runPromiseExit"] = (effect, options) =>
|
|
37
|
+
managed.runPromiseExit(withAmbient(effect), options);
|
|
38
|
+
|
|
39
|
+
const runtime: PlatformRuntime<Services, BuildError> = {
|
|
40
|
+
contextEffect: Effect.map(managed.contextEffect, (base) => {
|
|
41
|
+
const ambient = currentServices();
|
|
42
|
+
return ambient === undefined ? base : Context.merge(base, ambient);
|
|
43
|
+
}),
|
|
44
|
+
currentServices,
|
|
45
|
+
dispose: () => managed.dispose(),
|
|
46
|
+
runPromise: async (effect, options) => {
|
|
47
|
+
const exit = await runPromiseExit(effect, options);
|
|
48
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
49
|
+
throw Cause.squash(exit.cause);
|
|
50
|
+
},
|
|
51
|
+
runPromiseExit,
|
|
52
|
+
runWithServices: (context, evaluate) =>
|
|
53
|
+
services.run(context as Context.Context<never>, evaluate),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return runtime;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const makePlatformRuntime = <Services, BuildError>(
|
|
60
|
+
layer: Layer.Layer<Services, BuildError>,
|
|
61
|
+
options: PlatformRuntimeOptions = {},
|
|
62
|
+
): PlatformRuntime<Services, BuildError> => {
|
|
63
|
+
const key =
|
|
64
|
+
process.env.NODE_ENV === "production"
|
|
65
|
+
? undefined
|
|
66
|
+
: options.developmentCacheKey;
|
|
67
|
+
if (key === undefined) return make(layer);
|
|
68
|
+
|
|
69
|
+
const cached = runtimeCache.get(key);
|
|
70
|
+
if (cached !== undefined) {
|
|
71
|
+
return cached as PlatformRuntime<Services, BuildError>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const runtime = make(layer);
|
|
75
|
+
const cachedRuntime: PlatformRuntime<Services, BuildError> = {
|
|
76
|
+
...runtime,
|
|
77
|
+
dispose: async () => {
|
|
78
|
+
if (runtimeCache.get(key) === cachedRuntime) runtimeCache.delete(key);
|
|
79
|
+
await runtime.dispose();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
runtimeCache.set(key, cachedRuntime as PlatformRuntime<unknown, unknown>);
|
|
83
|
+
return cachedRuntime;
|
|
84
|
+
};
|