Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "zenstack-v3",
"displayName": "ZenStack",
"description": "ZenStack",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-adapters/better-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/better-auth",
"displayName": "ZenStack Better Auth Adapter",
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/cli",
"displayName": "ZenStack CLI",
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/clients/client-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/client-helpers",
"displayName": "ZenStack Client Helpers",
"description": "Helpers for implementing clients that consume ZenStack's CRUD service",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/clients/fetch-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/fetch-client",
"displayName": "ZenStack Fetch Client",
"description": "Simple fetch-based client for consuming ZenStack's RPC-style CRUD API",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/clients/tanstack-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/tanstack-query",
"displayName": "ZenStack TanStack Query Integration",
"description": "TanStack Query Client for consuming ZenStack v3's CRUD service",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/common-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/common-helpers",
"displayName": "ZenStack Common Helpers",
"description": "ZenStack Common Helpers",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/eslint-config",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"private": true,
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion packages/config/tsdown-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/tsdown-config",
"version": "3.8.0",
"version": "3.8.1",
"private": true,
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/typescript-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/typescript-config",
"version": "3.8.0",
"version": "3.8.1",
"private": true,
"license": "MIT"
}
2 changes: 1 addition & 1 deletion packages/config/vitest-config/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/vitest-config",
"type": "module",
"version": "3.8.0",
"version": "3.8.1",
"private": true,
"license": "MIT",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-zenstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "create-zenstack",
"displayName": "Create ZenStack",
"description": "Create a new ZenStack project",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/ide/vscode/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zenstack-v3",
"publisher": "zenstack",
"version": "3.8.0",
"version": "3.8.1",
"displayName": "ZenStack V3 Language Tools",
"description": "VSCode extension for ZenStack (v3) ZModel language",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/language",
"displayName": "ZenStack Language Tooling",
"description": "ZenStack ZModel language specification",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/orm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/orm",
"displayName": "ZenStack ORM",
"description": "ZenStack ORM",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
84 changes: 83 additions & 1 deletion packages/orm/src/client/query-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,38 @@ export function hasModel(schema: SchemaDef, model: string) {
.includes(model.toLowerCase());
}

/**
* Structural lookups derived purely from the (immutable) schema. The schema does not
* change for a client's lifetime, so these results are memoized per-schema and shared
* across every consumer (orm query building + plugins). Keyed by the schema object so
* the cache dies with the schema and never leaks across schemas. See issue #2715.
*/
interface SchemaLookupCache {
model: Map<string, ModelDef | undefined>;
m2mRelation: Map<string, ReturnType<typeof computeManyToManyRelation>>;
m2mJoinTable?: Map<string, ManyToManyJoinTableEndpoints | undefined>;
}

const schemaLookupCache = new WeakMap<SchemaDef, SchemaLookupCache>();

function getSchemaLookupCache(schema: SchemaDef): SchemaLookupCache {
let cache = schemaLookupCache.get(schema);
if (!cache) {
cache = { model: new Map(), m2mRelation: new Map() };
schemaLookupCache.set(schema, cache);
}
return cache;
}

export function getModel(schema: SchemaDef, model: string) {
return Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase());
const cache = getSchemaLookupCache(schema);
const key = model.toLowerCase();
if (cache.model.has(key)) {
return cache.model.get(key);
}
const result = Object.values(schema.models).find((m) => m.name.toLowerCase() === key);
cache.model.set(key, result);
return result;
}

export function getTypeDef(schema: SchemaDef, type: string) {
Expand Down Expand Up @@ -260,6 +290,58 @@ export function makeDefaultOrderBy(schema: SchemaDef, model: string) {
}

export function getManyToManyRelation(schema: SchemaDef, model: string, field: string) {
const cache = getSchemaLookupCache(schema);
const key = `${model} ${field}`;
if (cache.m2mRelation.has(key)) {
return cache.m2mRelation.get(key);
}
const result = computeManyToManyRelation(schema, model, field);
cache.m2mRelation.set(key, result);
return result;
}

/**
* Endpoints of an implicit many-to-many relation, identified by its join table name.
*/
export interface ManyToManyJoinTableEndpoints {
model: string;
field: string;
otherModel: string;
otherField: string;
}

/**
* Resolve the relation endpoints for an implicit many-to-many join table by its table
* name, or `undefined` if the table is not an implicit m2m join table. The join-table
* index is built once per schema (single pass) and reused, making this an O(1) lookup.
* See issue #2715.
*/
export function getManyToManyJoinTable(
schema: SchemaDef,
joinTableName: string,
): ManyToManyJoinTableEndpoints | undefined {
const cache = getSchemaLookupCache(schema);
if (!cache.m2mJoinTable) {
const map = new Map<string, ManyToManyJoinTableEndpoints>();
for (const model of Object.values(schema.models)) {
for (const field of Object.values(model.fields)) {
const m2m = getManyToManyRelation(schema, model.name, field.name);
if (m2m?.joinTable && !map.has(m2m.joinTable)) {
map.set(m2m.joinTable, {
model: model.name,
field: field.name,
otherModel: m2m.otherModel,
otherField: m2m.otherField,
});
}
}
}
cache.m2mJoinTable = map;
}
return cache.m2mJoinTable.get(joinTableName);
}

function computeManyToManyRelation(schema: SchemaDef, model: string, field: string) {
const fieldDef = requireField(schema, model, field);
if (!fieldDef.array || !fieldDef.relation?.opposite) {
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/policy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/plugin-policy",
"displayName": "ZenStack Access Policy Plugin",
"description": "ZenStack plugin that enforces access control policies defined in the schema",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
65 changes: 32 additions & 33 deletions packages/plugins/policy/src/policy-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1290,40 +1290,39 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf
}

private resolveManyToManyJoinTable(tableName: string) {
for (const model of Object.values(this.client.$schema.models)) {
for (const field of Object.values(model.fields)) {
const m2m = QueryUtils.getManyToManyRelation(this.client.$schema, model.name, field.name);
if (m2m?.joinTable === tableName) {
const sortedRecord = [
{
model: model.name,
field: field.name,
},
{
model: m2m.otherModel,
field: m2m.otherField,
},
].sort(this.manyToManySorter);

const firstIdFields = QueryUtils.requireIdFields(this.client.$schema, sortedRecord[0]!.model);
const secondIdFields = QueryUtils.requireIdFields(this.client.$schema, sortedRecord[1]!.model);
invariant(
firstIdFields.length === 1 && secondIdFields.length === 1,
'only single-field id is supported for implicit many-to-many join table',
);

return {
firstModel: sortedRecord[0]!.model,
firstField: sortedRecord[0]!.field,
firstIdField: firstIdFields[0]!,
secondModel: sortedRecord[1]!.model,
secondField: sortedRecord[1]!.field,
secondIdField: secondIdFields[0]!,
};
}
}
// O(1) lookup backed by a per-schema index (built once); previously this scanned
// the entire schema on every call for every table. See issue #2715.
const endpoints = QueryUtils.getManyToManyJoinTable(this.client.$schema, tableName);
if (!endpoints) {
return undefined;
}
return undefined;

const sortedRecord = [
{
model: endpoints.model,
field: endpoints.field,
},
{
model: endpoints.otherModel,
field: endpoints.otherField,
},
].sort(this.manyToManySorter);

const firstIdFields = QueryUtils.requireIdFields(this.client.$schema, sortedRecord[0]!.model);
const secondIdFields = QueryUtils.requireIdFields(this.client.$schema, sortedRecord[1]!.model);
invariant(
firstIdFields.length === 1 && secondIdFields.length === 1,
'only single-field id is supported for implicit many-to-many join table',
);

return {
firstModel: sortedRecord[0]!.model,
firstField: sortedRecord[0]!.field,
firstIdField: firstIdFields[0]!,
secondModel: sortedRecord[1]!.model,
secondField: sortedRecord[1]!.field,
secondIdField: secondIdFields[0]!,
};
}

private manyToManySorter(a: { model: string; field: string }, b: { model: string; field: string }): number {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/soft-delete/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/plugin-soft-delete",
"displayName": "ZenStack Soft Delete Plugin",
"description": "ZenStack plugin that implements soft-delete by intercepting Kysely queries",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/schema",
"displayName": "ZenStack Schema Object Model",
"description": "TypeScript representation of ZModel schema",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/sdk",
"displayName": "ZenStack SDK",
"description": "Utilities for building ZenStack plugins",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/server",
"displayName": "ZenStack Automatic CRUD Server",
"description": "ZenStack automatic CRUD API handlers and server adapters for popular frameworks",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/testtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/testtools",
"displayName": "ZenStack Test Tools",
"description": "ZenStack Test Tools",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion packages/zod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/zod",
"displayName": "ZenStack Zod Integration",
"description": "Automatically deriving Zod schemas from ZModel schemas",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
2 changes: 1 addition & 1 deletion samples/orm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sample-orm",
"version": "3.8.0",
"version": "3.8.1",
"description": "",
"main": "index.js",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion samples/taskforge/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "taskforge",
"version": "3.8.0",
"version": "3.8.1",
"type": "module",
"private": true,
"description": "A CLI for a team collaboration / project-tracking platform, built on ZenStack v3 (ORM) and better-auth.",
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "e2e",
"version": "3.8.0",
"version": "3.8.1",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion tests/regression/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "regression",
"version": "3.8.0",
"version": "3.8.1",
"private": true,
"type": "module",
"scripts": {
Expand Down
Loading
Loading