diff --git a/android/src/main/java/com/prisma/PrismaModule.java b/android/src/main/java/com/prisma/PrismaModule.java index 97d5d43d..2b858092 100644 --- a/android/src/main/java/com/prisma/PrismaModule.java +++ b/android/src/main/java/com/prisma/PrismaModule.java @@ -122,7 +122,17 @@ public void createDir(File dir) throws IOException public void install() { ReactApplicationContext context = this.getReactApplicationContext(); long jsContextPointer = context.getJavaScriptContextHolder().get(); - CallInvokerHolderImpl jsCallInvokerHolder = (CallInvokerHolderImpl)context.getCatalystInstance().getJSCallInvokerHolder(); + if (jsContextPointer == 0) { + throw new RuntimeException("JSI runtime pointer is null. Make sure Hermes is enabled and the runtime is initialized before calling install()."); + } + CallInvokerHolderImpl jsCallInvokerHolder; + if (context.getJSCallInvokerHolder() != null) { + jsCallInvokerHolder = (CallInvokerHolderImpl) context.getJSCallInvokerHolder(); + } else if (context.getCatalystInstance() != null) { + jsCallInvokerHolder = (CallInvokerHolderImpl) context.getCatalystInstance().getJSCallInvokerHolder(); + } else { + throw new RuntimeException("JSCallInvokerHolder is not available yet."); + } String dbPath = context.getDatabasePath("defaultDatabase").getAbsolutePath().replace("defaultDatabase", ""); String migrationsPath; try { diff --git a/copy-migrations.sh b/copy-migrations.sh index 43205c26..e97a21dd 100755 --- a/copy-migrations.sh +++ b/copy-migrations.sh @@ -5,7 +5,6 @@ echo "Copying prisma migration files..." MIGRATIONS_TARGET=${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH} rm -rf "$MIGRATIONS_TARGET/migrations" -mkdir "$MIGRATIONS_TARGET/migrations" -cp -r ${SRCROOT}/../migrations ${MIGRATIONS_TARGET} +cp -r ${SRCROOT}/../prisma/migrations "${MIGRATIONS_TARGET}/migrations" echo "migration files copied ✅" \ No newline at end of file diff --git a/cpp/QueryEngineHostObject.h b/cpp/QueryEngineHostObject.h index ec18d023..fa78dada 100644 --- a/cpp/QueryEngineHostObject.h +++ b/cpp/QueryEngineHostObject.h @@ -2,7 +2,12 @@ #ifndef query_engine_host_object_h #define query_engine_host_object_h -#include "query_engine.h" +#include +#if TARGET_OS_SIMULATOR +#include "../engines/ios/QueryEngine.xcframework/ios-arm64_x86_64-simulator/Headers/query_engine.h" +#else +#include "../engines/ios/QueryEngine.xcframework/ios-arm64/Headers/query_engine.h" +#endif #include #include #include diff --git a/cpp/react-native-prisma.cpp b/cpp/react-native-prisma.cpp index 387fbd0b..8cdd1ef0 100644 --- a/cpp/react-native-prisma.cpp +++ b/cpp/react-native-prisma.cpp @@ -4,6 +4,7 @@ #include "macros.h" #include "query_engine.h" #include "utils.h" +#include #include #include @@ -18,6 +19,12 @@ std::unordered_map> engine_map; ThreadPool thread_pool; +static void free_engine_string(const char *value) { + if (value != nullptr) { + free(const_cast(value)); + } +} + // Pure C function that is used by Rust to call the log callback extern void log_callback(const char *id, const char *msg) { if (engine_map.count(id)) { @@ -81,7 +88,7 @@ void install_cxx(jsi::Runtime &rt, } auto log_callback_fn = [&rt, js_log_callback](std::string msg) { - call_invoker->invokeAsync([&rt, msg, &js_log_callback] { + call_invoker->invokeAsync([&rt, msg, js_log_callback] { js_log_callback->asObject(rt).asFunction(rt).call( rt, jsi::String::createFromUtf8(rt, msg)); }); @@ -130,7 +137,7 @@ void install_cxx(jsi::Runtime &rt, auto resolve = std::make_shared(rt, args[0]); auto reject = std::make_shared(rt, args[1]); - auto task = [&rt, &queryEngineHostObject, body = std::move(body), + auto task = [&rt, queryEngineHostObject, body = std::move(body), trace = std::move(trace), tx_id = std::move(tx_id), resolve, reject]() { const char *response; @@ -147,8 +154,10 @@ void install_cxx(jsi::Runtime &rt, call_invoker->invokeAsync([&rt, response = std::move(response), error_ptr, resolve, reject]() { if (error_ptr == nullptr) { + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); resolve->asObject(rt).asFunction(rt).call( - rt, jsi::String::createFromUtf8(rt, response)); + rt, std::move(js_response)); } else { auto errCtr = rt.global().getPropertyAsFunction(rt, "Error"); std::string error_message(error_ptr); @@ -170,6 +179,38 @@ void install_cxx(jsi::Runtime &rt, return promise; }); + auto execute_sync = HOSTFN("executeSync", 4) { + std::shared_ptr queryEngineHostObject = + args[0].asObject(rt).asHostObject(rt); + std::string body = args[1].asString(rt).utf8(rt); + std::string trace = args[2].asString(rt).utf8(rt); + std::string tx_id; + if (count > 3 && args[3].isString()) { + tx_id = args[3].asString(rt).utf8(rt); + } + + const char *response = nullptr; + char *error_ptr = nullptr; + + if (!tx_id.empty()) { + response = prisma_query(queryEngineHostObject->engine, body.c_str(), + trace.c_str(), tx_id.c_str(), &error_ptr); + } else { + response = prisma_query(queryEngineHostObject->engine, body.c_str(), + trace.c_str(), nullptr, &error_ptr); + } + + if (error_ptr == nullptr) { + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); + return js_response; + } + + std::string error_message(error_ptr); + free(error_ptr); + throw std::runtime_error(error_message); + }); + auto start_transaction = HOSTFN("startTransaction", 3) { std::shared_ptr queryEngineHostObject = args[0].asObject(rt).asHostObject(rt); @@ -183,7 +224,9 @@ void install_cxx(jsi::Runtime &rt, throw std::runtime_error("prisma engine did not start transaction"); } - return jsi::String::createFromUtf8(rt, std::string(response)); + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); + return js_response; }); auto commit_transaction = HOSTFN("commitTransaction", 3) { @@ -199,7 +242,9 @@ void install_cxx(jsi::Runtime &rt, throw std::runtime_error("prisma engine did not commit transaction"); } - return jsi::String::createFromUtf8(rt, std::string(response)); + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); + return js_response; }); auto rollback_transaction = HOSTFN("rollbackTransaction", 3) { @@ -215,7 +260,9 @@ void install_cxx(jsi::Runtime &rt, throw std::runtime_error("prisma engine did not rollback transaction"); } - return jsi::String::createFromUtf8(rt, std::string(response)); + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); + return js_response; }); auto disconnect = HOSTFN("disconnect", 2) { @@ -253,6 +300,7 @@ void install_cxx(jsi::Runtime &rt, module.setProperty(rt, "create", std::move(create)); module.setProperty(rt, "connect", std::move(connect)); module.setProperty(rt, "execute", std::move(execute)); + module.setProperty(rt, "executeSync", std::move(execute_sync)); module.setProperty(rt, "startTransaction", std::move(start_transaction)); module.setProperty(rt, "commitTransaction", std::move(commit_transaction)); module.setProperty(rt, "rollbackTransaction", diff --git a/ios/Prisma.mm b/ios/Prisma.mm index 5898a16b..1bdd2331 100644 --- a/ios/Prisma.mm +++ b/ios/Prisma.mm @@ -1,52 +1,119 @@ #import "Prisma.h" #import +#import #import +#import +#import #import +#import #import +#ifdef RCT_NEW_ARCH_ENABLED +#import "RNPrismaSpecJSI.h" +#endif + +// Forward declare runtimeExecutor to silence selector warnings on older headers +@interface RCTBridge (RuntimeExecutorForwardDecl) +- (facebook::react::RuntimeExecutor)runtimeExecutor; +@end + +static NSString *PrismaLibraryPath() { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, true); + return [paths objectAtIndex:0]; +} + +static NSString *PrismaMigrationsPath() { + auto bundleURL = NSBundle.mainBundle.bundleURL; + auto migrations_path_absolute = [NSString stringWithFormat:@"%@%@", bundleURL.absoluteString, @"migrations"]; + return [migrations_path_absolute stringByReplacingOccurrencesOfString:@"file://" withString:@""]; +} + +static inline void InstallInRuntime(facebook::jsi::Runtime &runtime, + std::shared_ptr callInvoker) { + NSString *libraryPath = PrismaLibraryPath(); + NSString *migrationsPath = PrismaMigrationsPath(); + prisma::install_cxx(runtime, callInvoker, [libraryPath UTF8String], [migrationsPath UTF8String]); +} + +static RCTBridge *ResolveBridge(id module) { + RCTBridge *bridge = nil; + if ([module respondsToSelector:@selector(bridge)]) { + bridge = ((id)module).bridge; + } + if (bridge == nil) { + bridge = [RCTBridge currentBridge]; + } + return bridge; +} + @implementation Prisma @synthesize bridge=_bridge; RCT_EXPORT_MODULE() +// Old-arch sync install for classic bridge +#if !RCT_NEW_ARCH_ENABLED RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) { - RCTCxxBridge *cxxBridge = (RCTCxxBridge *)_bridge; - if (cxxBridge == nil) { - return @false; +#if DEBUG + std::cout << "▲ NSHomeDirectory:\n" << [NSHomeDirectory() UTF8String] << std::endl; + std::cout << "▲ Library Path:\n" << [PrismaLibraryPath() UTF8String] << std::endl; + std::cout << "▲ Migrations Path:\n" << [PrismaMigrationsPath() UTF8String] << std::endl; +#endif + + BOOL ok = NO; + auto okPtr = &ok; + RCTBridge *bridge = ResolveBridge(self); + + // Try new-arch path first if runtimeExecutor is available + if (bridge && [bridge respondsToSelector:@selector(runtimeExecutor)]) { + facebook::react::RuntimeExecutor executor = RCTRuntimeExecutorFromBridge(bridge); + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + executor([&](facebook::jsi::Runtime &runtime) { + InstallInRuntime(runtime, bridge.jsCallInvoker); + *okPtr = YES; + dispatch_semaphore_signal(sema); + }); + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + return @(ok); } - - auto jsiRuntime = (facebook::jsi::Runtime *)cxxBridge.runtime; - if (jsiRuntime == nil) { + + // Fallback to old-arch bridge access + RCTBridge *legacyBridge = _bridge ?: bridge; + RCTCxxBridge *cxxBridge = (RCTCxxBridge *)legacyBridge; + if (cxxBridge == nil || cxxBridge.runtime == nil) { + NSLog(@"[Prisma] no runtime available to install cxx"); return @false; } + + auto jsiRuntime = (facebook::jsi::Runtime *)cxxBridge.runtime; auto &runtime = *jsiRuntime; - auto callInvoker = _bridge.jsCallInvoker; - - // get migrations folder - auto bundleURL = NSBundle.mainBundle.bundleURL; - auto migrations_path_absolute = [NSString stringWithFormat:@"%@%@", bundleURL.absoluteString, @"migrations"]; - auto migrations_path = [migrations_path_absolute stringByReplacingOccurrencesOfString:@"file://" withString:@""]; - - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, true); - NSString *libraryPath = [paths objectAtIndex:0]; - -#if DEBUG - std::cout << "▲ NSHomeDirectory:\n" << [NSHomeDirectory() UTF8String] << std::endl; - std::cout << "▲ Library Path:\n" << [libraryPath UTF8String] << std::endl; - std::cout << "▲ Migrations Path:\n" << [migrations_path UTF8String] << std::endl; -#endif + auto callInvoker = _bridge ? _bridge.jsCallInvoker : legacyBridge.jsCallInvoker; - prisma::install_cxx(runtime, callInvoker, [libraryPath UTF8String], [migrations_path UTF8String]); - return nil; + InstallInRuntime(runtime, callInvoker); + return @true; } +#else +// Required by NativePrismaSpec protocol in new-arch; TurboModule path uses C++ spec below. +- (void)install {} +#endif #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(params); + class PrismaCxxTurboModule final : public facebook::react::NativePrismaCxxSpec { + public: + explicit PrismaCxxTurboModule(std::shared_ptr jsInvoker) + : NativePrismaCxxSpec(std::move(jsInvoker)) {} + + void install(facebook::jsi::Runtime &rt) { + InstallInRuntime(rt, this->jsInvoker_); + } + }; + + return std::make_shared(params.jsInvoker); } #endif diff --git a/package.json b/package.json index 3845e2ca..470d0d24 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "!**/__mocks__", "!**/.*", "copy-migrations.sh", + "scripts/patch-prisma-runtime.cjs", "react-native-prisma.gradle", "plugin", "app.plugin.js", @@ -42,6 +43,7 @@ "lint": "eslint \"**/*.{js,ts,tsx}\"", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", "prepare": "yarn download-engine && bob build", + "postinstall": "node ./scripts/patch-prisma-runtime.cjs", "ios:full": "yarn qe:sim && cd example && yarn ios", "clang": "clang-format -i ./cpp/*.cpp ./cpp/*.h && git add .", "ios": "cd example && yarn ios", diff --git a/scripts/patch-prisma-runtime.cjs b/scripts/patch-prisma-runtime.cjs new file mode 100644 index 00000000..7e017dc0 --- /dev/null +++ b/scripts/patch-prisma-runtime.cjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +const fs = require('node:fs'); + +const marker = '__prismaReactNativeFastRead'; + +function resolveRuntime() { + const paths = [process.env.INIT_CWD, process.cwd()].filter(Boolean); + return require.resolve('@prisma/client/runtime/react-native.js', { paths }); +} + +function patchRuntime(runtimePath) { + let source = fs.readFileSync(runtimePath, 'utf8'); + + if (source.includes(marker)) { + return false; + } + + source = source.replace( + '"use strict";', + `"use strict";var ${marker}=e=>{try{let t=e?.query?.arguments??{};if(t.distinct!=null)return!1;if(e?.action==="findUnique"||e?.action==="findFirst")return!0;return e?.action==="findMany"&&typeof t.take==="number"&&Number.isFinite(t.take)&&Math.abs(t.take)<=100}catch{return!1}};` + ); + + const queryMethod = + 'query(t,r,n,i){return __PrismaProxy.execute(this.engineObject,t,r,n,i)}compile(){throw new Error("not implemented")}'; + const querySyncMethod = + 'query(t,r,n,i){return __PrismaProxy.execute(this.engineObject,t,r,n,i)}querySync(t,r,n,i){return globalThis.__PrismaProxy.executeSync(this.engineObject,t,r,n,i)}compile(){throw new Error("not implemented")}'; + + if (!source.includes(queryMethod)) { + throw new Error( + `Unsupported @prisma/client react-native QueryEngine shape: ${runtimePath}` + ); + } + + source = source.replace(queryMethod, querySyncMethod); + + const wrapEngineQuery = + 'metrics:t.metrics?.bind(t),query:this.withRequestId(t.query.bind(t)),rollbackTransaction:this.withRequestId(t.rollbackTransaction.bind(t))'; + const wrapEngineQuerySync = + 'metrics:t.metrics?.bind(t),query:this.withRequestId(t.query.bind(t)),querySync:t.querySync?.bind(t),rollbackTransaction:this.withRequestId(t.rollbackTransaction.bind(t))'; + + if (!source.includes(wrapEngineQuery)) { + throw new Error( + `Unsupported @prisma/client react-native wrapEngine shape: ${runtimePath}` + ); + } + + source = source.replace(wrapEngineQuery, wrapEngineQuerySync); + + const requestStart = + 'try{await this.start();let s=await this.adapterPromise;this.executingQueryPromise=this.engine?.query(o,i,n?.id),this.lastQuery=o;let a=this.parseEngineResponse(await this.executingQueryPromise);'; + const fastRequest = + 'try{if(this.libraryStarted&&this.engine&&!n){let s=this.adapterPromise?await this.adapterPromise:void 0;if(this.engine.querySync&&globalThis.__PrismaProxy?.executeSync&&__prismaReactNativeFastRead(t)){this.lastQuery=o;let r=this.parseEngineResponse(this.engine.querySync(o,i,void 0));if(r.errors)throw r.errors.length===1?this.buildQueryError(r.errors[0],s?.errorRegistry):new G(JSON.stringify(r.errors),{clientVersion:this.config.clientVersion});if(this.loggerRustPanic)throw this.loggerRustPanic;return{data:r}}this.executingQueryPromise=this.engine.query(o,i,void 0),this.lastQuery=o;let r=this.parseEngineResponse(await this.executingQueryPromise);if(r.errors)throw r.errors.length===1?this.buildQueryError(r.errors[0],s?.errorRegistry):new G(JSON.stringify(r.errors),{clientVersion:this.config.clientVersion});if(this.loggerRustPanic)throw this.loggerRustPanic;return{data:r}}await this.start();let s=await this.adapterPromise;this.executingQueryPromise=this.engine?.query(o,i,n?.id),this.lastQuery=o;let a=this.parseEngineResponse(await this.executingQueryPromise);'; + + if (!source.includes(requestStart)) { + throw new Error( + `Unsupported @prisma/client react-native runtime shape: ${runtimePath}` + ); + } + + source = source.replace(requestStart, fastRequest); + fs.writeFileSync(runtimePath, source); + return true; +} + +try { + const runtimePath = resolveRuntime(); + const patched = patchRuntime(runtimePath); + if (patched) { + console.log(`Patched Prisma React Native runtime: ${runtimePath}`); + } +} catch (error) { + console.warn( + `Could not patch Prisma React Native runtime: ${ + error instanceof Error ? error.message : String(error) + }` + ); +} diff --git a/src/index.ts b/src/index.ts index a448579c..45f44d17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,8 @@ if (!global.__PrismaProxy) { // Wrap the create function to stringify the env variables if necessary const ogCreate = __PrismaProxy!.create; +const ogExecute = __PrismaProxy!.execute; + global.__PrismaProxy = { ...global.__PrismaProxy, create: (options: PrismaCreateOptions): QueryEngineObject => { @@ -43,6 +45,14 @@ global.__PrismaProxy = { } return ogCreate(options); }, + execute: ( + engine: QueryEngineObject, + body: string, + headers: string, + txId?: string + ): Promise => { + return ogExecute(engine, body, headers, txId); + }, }; type PrismaCreateOptions = { @@ -64,8 +74,14 @@ type PrismaProxy = { engine: QueryEngineObject, body: string, headers: string, - txId: string + txId?: string ) => Promise; + executeSync?: ( + engine: QueryEngineObject, + body: string, + headers: string, + txId?: string + ) => string; startTransaction: ( engine: QueryEngineObject, body: string,