@gblikas/querykit 0.0.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.
Files changed (118) hide show
  1. package/.cursor/BUGBOT.md +21 -0
  2. package/.cursor/rules/01-project-structure.mdc +77 -0
  3. package/.cursor/rules/02-typescript-standards.mdc +105 -0
  4. package/.cursor/rules/03-testing-standards.mdc +78 -0
  5. package/.cursor/rules/04-query-language.mdc +79 -0
  6. package/.cursor/rules/05-solid-principles.mdc +118 -0
  7. package/.cursor/rules/liqe-readme-docs.mdc +438 -0
  8. package/.devcontainer/devcontainer.json +25 -0
  9. package/.eslintignore +1 -0
  10. package/.eslintrc.js +39 -0
  11. package/.github/dependabot.yml +12 -0
  12. package/.github/workflows/ci.yml +114 -0
  13. package/.github/workflows/publish.yml +61 -0
  14. package/.husky/pre-commit +30 -0
  15. package/.prettierrc +10 -0
  16. package/CONTRIBUTING.md +187 -0
  17. package/LICENSE +674 -0
  18. package/README.md +237 -0
  19. package/dist/adapters/drizzle/index.d.ts +122 -0
  20. package/dist/adapters/drizzle/index.js +166 -0
  21. package/dist/adapters/index.d.ts +7 -0
  22. package/dist/adapters/index.js +25 -0
  23. package/dist/adapters/types.d.ts +60 -0
  24. package/dist/adapters/types.js +8 -0
  25. package/dist/index.d.ts +75 -0
  26. package/dist/index.js +118 -0
  27. package/dist/parser/index.d.ts +2 -0
  28. package/dist/parser/index.js +18 -0
  29. package/dist/parser/parser.d.ts +51 -0
  30. package/dist/parser/parser.js +201 -0
  31. package/dist/parser/types.d.ts +68 -0
  32. package/dist/parser/types.js +5 -0
  33. package/dist/query/builder.d.ts +61 -0
  34. package/dist/query/builder.js +188 -0
  35. package/dist/query/index.d.ts +2 -0
  36. package/dist/query/index.js +18 -0
  37. package/dist/query/types.d.ts +79 -0
  38. package/dist/query/types.js +2 -0
  39. package/dist/security/index.d.ts +2 -0
  40. package/dist/security/index.js +18 -0
  41. package/dist/security/types.d.ts +181 -0
  42. package/dist/security/types.js +43 -0
  43. package/dist/security/validator.d.ts +191 -0
  44. package/dist/security/validator.js +344 -0
  45. package/dist/translators/drizzle/index.d.ts +73 -0
  46. package/dist/translators/drizzle/index.js +260 -0
  47. package/dist/translators/index.d.ts +8 -0
  48. package/dist/translators/index.js +27 -0
  49. package/dist/translators/sql/index.d.ts +108 -0
  50. package/dist/translators/sql/index.js +252 -0
  51. package/dist/translators/types.d.ts +39 -0
  52. package/dist/translators/types.js +8 -0
  53. package/examples/qk-next/README.md +35 -0
  54. package/examples/qk-next/app/favicon.ico +0 -0
  55. package/examples/qk-next/app/globals.css +122 -0
  56. package/examples/qk-next/app/layout.tsx +121 -0
  57. package/examples/qk-next/app/page.tsx +813 -0
  58. package/examples/qk-next/app/providers.tsx +80 -0
  59. package/examples/qk-next/components/aurora-background.tsx +12 -0
  60. package/examples/qk-next/components/github-stars.tsx +51 -0
  61. package/examples/qk-next/components/mode-toggle.tsx +27 -0
  62. package/examples/qk-next/components/reactbits/blocks/Backgrounds/Aurora/Aurora.tsx +217 -0
  63. package/examples/qk-next/components/reactbits/blocks/Backgrounds/LightRays/LightRays.tsx +474 -0
  64. package/examples/qk-next/components/theme-provider.tsx +11 -0
  65. package/examples/qk-next/components/ui/card.tsx +92 -0
  66. package/examples/qk-next/components/ui/command.tsx +184 -0
  67. package/examples/qk-next/components/ui/dialog.tsx +143 -0
  68. package/examples/qk-next/components/ui/drawer.tsx +135 -0
  69. package/examples/qk-next/components/ui/hover-card.tsx +44 -0
  70. package/examples/qk-next/components/ui/icons.tsx +148 -0
  71. package/examples/qk-next/components/ui/sonner.tsx +26 -0
  72. package/examples/qk-next/components/ui/table.tsx +117 -0
  73. package/examples/qk-next/components.json +21 -0
  74. package/examples/qk-next/eslint.config.mjs +21 -0
  75. package/examples/qk-next/jsrepo.json +13 -0
  76. package/examples/qk-next/lib/utils.ts +6 -0
  77. package/examples/qk-next/next.config.ts +8 -0
  78. package/examples/qk-next/package.json +48 -0
  79. package/examples/qk-next/pnpm-lock.yaml +5558 -0
  80. package/examples/qk-next/postcss.config.mjs +5 -0
  81. package/examples/qk-next/public/file.svg +1 -0
  82. package/examples/qk-next/public/globe.svg +1 -0
  83. package/examples/qk-next/public/next.svg +1 -0
  84. package/examples/qk-next/public/vercel.svg +1 -0
  85. package/examples/qk-next/public/window.svg +1 -0
  86. package/examples/qk-next/tsconfig.json +42 -0
  87. package/examples/qk-next/types/sonner.d.ts +3 -0
  88. package/jest.config.js +26 -0
  89. package/package.json +51 -0
  90. package/src/adapters/drizzle/drizzle-adapter.test.ts +115 -0
  91. package/src/adapters/drizzle/index.ts +299 -0
  92. package/src/adapters/index.ts +11 -0
  93. package/src/adapters/types.ts +72 -0
  94. package/src/index.ts +194 -0
  95. package/src/integration.test.ts +202 -0
  96. package/src/parser/index.ts +2 -0
  97. package/src/parser/parser.test.ts +1056 -0
  98. package/src/parser/parser.ts +268 -0
  99. package/src/parser/types.ts +97 -0
  100. package/src/query/builder.test.ts +272 -0
  101. package/src/query/builder.ts +274 -0
  102. package/src/query/index.ts +2 -0
  103. package/src/query/types.ts +107 -0
  104. package/src/security/index.ts +2 -0
  105. package/src/security/types.ts +210 -0
  106. package/src/security/validator.test.ts +459 -0
  107. package/src/security/validator.ts +395 -0
  108. package/src/security.test.ts +366 -0
  109. package/src/translators/drizzle/drizzle-translator.test.ts +128 -0
  110. package/src/translators/drizzle/index.test.ts +45 -0
  111. package/src/translators/drizzle/index.ts +346 -0
  112. package/src/translators/index.ts +14 -0
  113. package/src/translators/sql/index.test.ts +45 -0
  114. package/src/translators/sql/index.ts +331 -0
  115. package/src/translators/sql/sql-translator.test.ts +419 -0
  116. package/src/translators/types.ts +44 -0
  117. package/src/types/sonner.d.ts +3 -0
  118. package/tsconfig.json +34 -0
package/src/index.ts ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * QueryKit - A comprehensive query toolkit for TypeScript
3
+ *
4
+ * QueryKit simplifies how you build and execute data queries across different
5
+ * environments with a unified, intuitive syntax for filtering, sorting, and
6
+ * transforming data.
7
+ */
8
+
9
+ // Core exports
10
+ import { QueryBuilder, IQueryBuilderOptions } from './query';
11
+ import { QueryParser, IParserOptions } from './parser';
12
+ import { SqlTranslator } from './translators/sql';
13
+ import { ISecurityOptions, QuerySecurityValidator } from './security';
14
+ import { IAdapter, IAdapterOptions } from './adapters';
15
+
16
+ export {
17
+ // Parser exports
18
+ QueryParser,
19
+ IParserOptions,
20
+
21
+ // Query builder exports
22
+ QueryBuilder,
23
+ IQueryBuilderOptions,
24
+
25
+ // Translator exports
26
+ SqlTranslator
27
+ };
28
+
29
+ // Re-export from modules
30
+ export * from './translators';
31
+ export * from './adapters';
32
+
33
+ /**
34
+ * Create a new QueryBuilder instance
35
+ */
36
+ export function createQueryBuilder<T>(
37
+ options?: IQueryBuilderOptions<T>
38
+ ): QueryBuilder<T> {
39
+ return new QueryBuilder<T>(options);
40
+ }
41
+
42
+ /**
43
+ * Create a new QueryParser instance
44
+ */
45
+ export function createQueryParser(options?: IParserOptions): QueryParser {
46
+ return new QueryParser(options);
47
+ }
48
+
49
+ /**
50
+ * Options for creating a new QueryKit instance
51
+ */
52
+ export interface IQueryKitOptions<
53
+ TSchema extends Record<string, object> = Record<
54
+ string,
55
+ Record<string, unknown>
56
+ >
57
+ > {
58
+ /**
59
+ * The adapter to use for database connections
60
+ */
61
+ adapter: IAdapter;
62
+
63
+ /**
64
+ * The schema to use for query validation
65
+ */
66
+ schema: TSchema;
67
+
68
+ /**
69
+ * Security options for query validation
70
+ */
71
+ security?: ISecurityOptions;
72
+
73
+ /**
74
+ * Options to initialize the provided adapter
75
+ */
76
+ adapterOptions?: IAdapterOptions & { [key: string]: unknown };
77
+ }
78
+
79
+ // Define interfaces for return types
80
+ export interface IQueryExecutor<TResult> {
81
+ execute(): Promise<TResult[]>;
82
+ orderBy(field: string, direction?: 'asc' | 'desc'): IQueryExecutor<TResult>;
83
+ limit(count: number): IQueryExecutor<TResult>;
84
+ offset(count: number): IQueryExecutor<TResult>;
85
+ }
86
+
87
+ export interface IWhereClause<TResult> {
88
+ where(queryString: string): IQueryExecutor<TResult>;
89
+ }
90
+
91
+ /**
92
+ * Public QueryKit type
93
+ */
94
+ export type QueryKit<
95
+ TSchema extends Record<string, object>,
96
+ TRows extends { [K in keyof TSchema & string]: unknown } = {
97
+ [K in keyof TSchema & string]: unknown;
98
+ }
99
+ > = {
100
+ query<K extends keyof TSchema & string>(table: K): IWhereClause<TRows[K]>;
101
+ };
102
+
103
+ /**
104
+ * Create a new QueryKit instance
105
+ */
106
+ export function createQueryKit<
107
+ TSchema extends Record<string, object>,
108
+ TRows extends { [K in keyof TSchema & string]: unknown } = {
109
+ [K in keyof TSchema & string]: unknown;
110
+ }
111
+ >(options: IQueryKitOptions<TSchema>): QueryKit<TSchema, TRows> {
112
+ const parser = new QueryParser();
113
+ const securityValidator = new QuerySecurityValidator(options.security);
114
+
115
+ // Initialize adapter if options provided. If adapter is already initialized,
116
+ // calling initialize again with the same options should be a no-op for most adapters.
117
+ if (options.adapterOptions) {
118
+ const mergedAdapterOptions: IAdapterOptions & { [key: string]: unknown } = {
119
+ // Ensure adapter receives schema information if not already provided
120
+ schema: options.adapterOptions.schema ?? options.schema,
121
+ ...options.adapterOptions
122
+ } as IAdapterOptions & { [key: string]: unknown };
123
+
124
+ try {
125
+ options.adapter.initialize(mergedAdapterOptions);
126
+ } catch {
127
+ // If initialization fails here, the adapter might already be initialized
128
+ // or require a different init path; we'll let execute-time errors surface.
129
+ }
130
+ }
131
+
132
+ // This function would be expanded to include all QueryKit functionality
133
+ return {
134
+ query: <K extends keyof TSchema & string>(
135
+ table: K
136
+ ): IWhereClause<TRows[K]> => {
137
+ return {
138
+ where: (queryString: string): IQueryExecutor<TRows[K]> => {
139
+ // Parse and validate the query
140
+ const expressionAst = parser.parse(queryString);
141
+ securityValidator.validate(
142
+ expressionAst,
143
+ options.schema as unknown as Record<string, Record<string, unknown>>
144
+ );
145
+
146
+ // Execution state accumulated via fluent calls
147
+ let orderByState: Record<string, 'asc' | 'desc'> = {};
148
+ let limitState: number | undefined;
149
+ let offsetState: number | undefined;
150
+
151
+ const executor: IQueryExecutor<TRows[K]> = {
152
+ orderBy: (
153
+ field: string,
154
+ direction: 'asc' | 'desc' = 'asc'
155
+ ): IQueryExecutor<TRows[K]> => {
156
+ orderByState = { ...orderByState, [field]: direction };
157
+ return executor;
158
+ },
159
+ limit: (count: number): IQueryExecutor<TRows[K]> => {
160
+ limitState = count;
161
+ return executor;
162
+ },
163
+ offset: (count: number): IQueryExecutor<TRows[K]> => {
164
+ offsetState = count;
165
+ return executor;
166
+ },
167
+ execute: async (): Promise<TRows[K][]> => {
168
+ // Delegate to adapter
169
+ const results = await options.adapter.execute(
170
+ table,
171
+ expressionAst,
172
+ {
173
+ orderBy:
174
+ Object.keys(orderByState).length > 0
175
+ ? orderByState
176
+ : undefined,
177
+ limit: limitState,
178
+ offset: offsetState
179
+ }
180
+ );
181
+ return results as TRows[K][];
182
+ }
183
+ };
184
+
185
+ return executor;
186
+ }
187
+ };
188
+ }
189
+ };
190
+ }
191
+
192
+ // Export all public APIs
193
+ export * from './parser';
194
+ export * from './security';
@@ -0,0 +1,202 @@
1
+ import { QueryParser } from './parser';
2
+ import { DrizzleTranslator } from './translators/drizzle';
3
+ import { DrizzleAdapter, IDrizzleDatabase } from './adapters/drizzle';
4
+ import { SQLWrapper, SQL, sql } from 'drizzle-orm';
5
+ import { createQueryBuilder, createQueryKit } from './index';
6
+
7
+ // Helper function to safely get SQL string value for testing
8
+ function getSqlString(sqlObj: SQL): string {
9
+ // For testing purposes only - extract a string representation
10
+ // of the SQL query that we can use in our assertions
11
+ try {
12
+ return JSON.stringify(sqlObj);
13
+ } catch (e) {
14
+ return String(sqlObj);
15
+ }
16
+ }
17
+
18
+ // Define a user type for testing
19
+ interface ITodo {
20
+ id: number;
21
+ title: string;
22
+ priority: number;
23
+ status: string;
24
+ }
25
+
26
+ describe('QueryKit Integration Tests', () => {
27
+ // Set up mocks for the Drizzle adapter
28
+ const mockWhere = jest.fn().mockReturnThis();
29
+ const mockOrderBy = jest.fn().mockReturnThis();
30
+ const mockLimit = jest.fn().mockReturnThis();
31
+ const mockOffset = jest.fn().mockReturnThis();
32
+ const mockFrom = jest.fn().mockReturnValue({
33
+ where: mockWhere,
34
+ orderBy: mockOrderBy,
35
+ limit: mockLimit,
36
+ offset: mockOffset,
37
+ then: <T>(callback: (value: ITodo[]) => T) =>
38
+ Promise.resolve(
39
+ callback([
40
+ { id: 1, title: 'Buy groceries', priority: 2, status: 'active' },
41
+ { id: 2, title: 'Fix bug', priority: 3, status: 'active' }
42
+ ])
43
+ )
44
+ });
45
+ const mockSelect = jest.fn().mockReturnValue({ from: mockFrom });
46
+
47
+ // Create a mock DB instance
48
+ const mockDb: IDrizzleDatabase = {
49
+ select: mockSelect
50
+ };
51
+
52
+ // Create mock schema with SQLWrapper fields
53
+ const mockSchema = {
54
+ todos: {
55
+ id: sql.raw('id') as unknown as SQLWrapper,
56
+ title: sql.raw('title') as unknown as SQLWrapper,
57
+ priority: sql.raw('priority') as unknown as SQLWrapper,
58
+ status: sql.raw('status') as unknown as SQLWrapper
59
+ }
60
+ };
61
+
62
+ // Set up the adapter
63
+ const adapter = new DrizzleAdapter();
64
+ adapter.initialize({
65
+ db: mockDb,
66
+ schema: mockSchema
67
+ });
68
+
69
+ beforeEach(() => {
70
+ jest.clearAllMocks();
71
+ });
72
+
73
+ describe('End-to-end Query', () => {
74
+ it('should parse, translate and execute a query', async () => {
75
+ // Parse a query expression
76
+ const parser = new QueryParser();
77
+ const expression = parser.parse('priority:>1 AND status:"active"');
78
+
79
+ // Execute the query using the adapter
80
+ const results = await adapter.execute<ITodo>('todos', expression);
81
+
82
+ // Verify results
83
+ expect(results).toHaveLength(2);
84
+ expect(results[0].title).toBe('Buy groceries');
85
+ expect(results[1].title).toBe('Fix bug');
86
+
87
+ // Verify that the mocks were called correctly
88
+ expect(mockSelect).toHaveBeenCalled();
89
+ expect(mockFrom).toHaveBeenCalled();
90
+ expect(mockWhere).toHaveBeenCalled();
91
+
92
+ // Verify the WHERE clause reflects the parsed query
93
+ const whereArg = mockWhere.mock.calls[0][0] as unknown as SQL;
94
+ const whereStr = getSqlString(whereArg);
95
+ expect(whereStr).toContain('priority');
96
+ expect(whereStr.toLowerCase()).toContain('active');
97
+ });
98
+
99
+ it('should work with the query builder', async () => {
100
+ // Create a query builder
101
+ const queryBuilder = createQueryBuilder<ITodo>();
102
+
103
+ // Build a query
104
+ queryBuilder.where('priority', '>', 1).andWhere('status', '==', 'active');
105
+
106
+ // Get the expression from the builder
107
+ const expression = queryBuilder.getExpression();
108
+
109
+ // Execute the query using the adapter
110
+ const results = await adapter.execute<ITodo>('todos', expression);
111
+
112
+ // Verify results
113
+ expect(results).toHaveLength(2);
114
+ expect(results[0].id).toBe(1);
115
+ expect(results[1].id).toBe(2);
116
+ });
117
+ });
118
+
119
+ describe('Translation Pipeline', () => {
120
+ it('should translate expressions consistently', () => {
121
+ // Parse a query string
122
+ const parser = new QueryParser();
123
+ const stringExpression = parser.parse('status:"active" OR priority:>2');
124
+
125
+ // Create the same expression with the builder
126
+ const queryBuilder = createQueryBuilder<ITodo>();
127
+ const builderExpression = queryBuilder
128
+ .where('status', '==', 'active')
129
+ .orWhere('priority', '>', 2)
130
+ .getExpression();
131
+
132
+ // Create a translator
133
+ const translator = new DrizzleTranslator();
134
+
135
+ // Translate both expressions
136
+ const stringTranslated = translator.translate(stringExpression);
137
+ const builderTranslated = translator.translate(builderExpression);
138
+
139
+ // The SQL from both translations should have the same structure
140
+ // (We can't directly compare SQL objects)
141
+ const stringTranslatedStr = getSqlString(stringTranslated);
142
+ const builderTranslatedStr = getSqlString(builderTranslated);
143
+
144
+ expect(stringTranslatedStr).toContain('OR');
145
+ expect(builderTranslatedStr).toContain('OR');
146
+
147
+ // Both translations should contain similar structural elements
148
+ // (Avoiding exact string matches as SQL objects might differ in representation)
149
+ if (
150
+ stringTranslatedStr.includes('status') &&
151
+ builderTranslatedStr.includes('status')
152
+ ) {
153
+ expect(true).toBe(true); // Both contain 'status'
154
+ } else {
155
+ expect(stringTranslatedStr).toContain('status');
156
+ expect(builderTranslatedStr).toContain('status');
157
+ }
158
+
159
+ if (
160
+ stringTranslatedStr.includes('priority') &&
161
+ builderTranslatedStr.includes('priority')
162
+ ) {
163
+ expect(true).toBe(true); // Both contain 'priority'
164
+ } else {
165
+ expect(stringTranslatedStr).toContain('priority');
166
+ expect(builderTranslatedStr).toContain('priority');
167
+ }
168
+ });
169
+ });
170
+
171
+ describe('Fluent API execution', () => {
172
+ it('executes a fluent query via createQueryKit', async () => {
173
+ const adapter = new DrizzleAdapter();
174
+ adapter.initialize({ db: mockDb, schema: mockSchema });
175
+
176
+ const qk = createQueryKit({
177
+ adapter,
178
+ schema: { todos: { id: {}, title: {}, priority: {}, status: {} } }
179
+ });
180
+
181
+ const results = await qk
182
+ .query('todos')
183
+ .where('priority:>1 AND status:"active"')
184
+ .orderBy('priority', 'desc')
185
+ .limit(10)
186
+ .execute();
187
+
188
+ expect(results).toHaveLength(2);
189
+ expect(mockSelect).toHaveBeenCalled();
190
+ expect(mockFrom).toHaveBeenCalled();
191
+ expect(mockWhere).toHaveBeenCalled();
192
+ expect(mockOrderBy).toHaveBeenCalled();
193
+ expect(mockLimit).toHaveBeenCalledWith(10);
194
+
195
+ // Verify WHERE clause from fluent path
196
+ const whereArg = mockWhere.mock.calls[0][0] as unknown as SQL;
197
+ const whereStr = getSqlString(whereArg);
198
+ expect(whereStr).toContain('priority');
199
+ expect(whereStr.toLowerCase()).toContain('active');
200
+ });
201
+ });
202
+ });
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './parser';