@stonyx/orm 0.3.2-beta.65 → 0.3.2-beta.66

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.
@@ -33,6 +33,9 @@ const {
33
33
  TIMESCALE_DATABASE,
34
34
  TIMESCALE_CONNECTION_LIMIT,
35
35
  TIMESCALE_MIGRATIONS_DIR,
36
+ DYNAMODB_REGION,
37
+ DYNAMODB_ENDPOINT,
38
+ DYNAMODB_TABLE_PREFIX,
36
39
  } = process.env;
37
40
 
38
41
  export default {
@@ -84,6 +87,11 @@ export default {
84
87
  migrationsDir: TIMESCALE_MIGRATIONS_DIR ?? 'migrations',
85
88
  migrationsTable: '__migrations',
86
89
  } : undefined,
90
+ dynamodb: DYNAMODB_REGION ? {
91
+ region: DYNAMODB_REGION,
92
+ endpoint: DYNAMODB_ENDPOINT || undefined,
93
+ tablePrefix: DYNAMODB_TABLE_PREFIX || '',
94
+ } : undefined,
87
95
  restServer: {
88
96
  enabled: ORM_USE_REST_SERVER ?? 'true', // Whether to load restServer for automatic route setup or
89
97
  route: ORM_REST_ROUTE ?? '/',
package/dist/commands.js CHANGED
@@ -20,6 +20,11 @@ const commands = {
20
20
  description: 'Generate a MySQL migration from current model schemas',
21
21
  bootstrap: true,
22
22
  run: async (args) => {
23
+ const config = (await import('stonyx/config')).default;
24
+ if (config.orm.dynamodb) {
25
+ console.log('DynamoDB does not use file-based migrations. Use db:sync to provision tables.');
26
+ return;
27
+ }
23
28
  const description = args?.join(' ') || 'migration';
24
29
  const { generateMigration } = await import('./mysql/migration-generator.js');
25
30
  const result = await generateMigration(description);
@@ -31,12 +36,33 @@ const commands = {
31
36
  }
32
37
  }
33
38
  },
39
+ 'db:sync': {
40
+ description: 'Provision DynamoDB tables and GSIs from current model schemas',
41
+ bootstrap: true,
42
+ run: async () => {
43
+ const config = (await import('stonyx/config')).default;
44
+ if (!config.orm.dynamodb) {
45
+ console.error('DynamoDB is not configured. Set DYNAMODB_REGION (and optionally DYNAMODB_ENDPOINT) to enable DynamoDB mode.');
46
+ process.exit(1);
47
+ }
48
+ const { default: DynamoDBDB } = await import('./dynamodb/dynamodb-db.js');
49
+ const db = new DynamoDBDB();
50
+ await db.init();
51
+ await db.startup();
52
+ await db.shutdown();
53
+ console.log('DynamoDB tables synced successfully.');
54
+ }
55
+ },
34
56
  'db:migrate': {
35
57
  description: 'Apply pending MySQL migrations',
36
58
  bootstrap: true,
37
59
  run: async () => {
38
60
  const config = (await import('stonyx/config')).default;
39
61
  const mysqlConfig = config.orm.mysql;
62
+ if (config.orm.dynamodb) {
63
+ console.log('DynamoDB does not use file-based migrations. Use db:sync to provision tables.');
64
+ return;
65
+ }
40
66
  if (!mysqlConfig) {
41
67
  console.error('MySQL is not configured. Set MYSQL_HOST to enable MySQL mode.');
42
68
  process.exit(1);
@@ -75,6 +101,10 @@ const commands = {
75
101
  bootstrap: true,
76
102
  run: async () => {
77
103
  const config = (await import('stonyx/config')).default;
104
+ if (config.orm.dynamodb) {
105
+ console.log('DynamoDB does not support migration rollback. Manage table changes via the AWS console or db:sync.');
106
+ return;
107
+ }
78
108
  const mysqlConfig = config.orm.mysql;
79
109
  if (!mysqlConfig) {
80
110
  console.error('MySQL is not configured. Set MYSQL_HOST to enable MySQL mode.');
@@ -113,6 +143,10 @@ const commands = {
113
143
  bootstrap: true,
114
144
  run: async () => {
115
145
  const config = (await import('stonyx/config')).default;
146
+ if (config.orm.dynamodb) {
147
+ console.log('DynamoDB does not use file-based migrations. Use db:sync to provision tables.');
148
+ return;
149
+ }
116
150
  const mysqlConfig = config.orm.mysql;
117
151
  if (!mysqlConfig) {
118
152
  console.error('MySQL is not configured. Set MYSQL_HOST to enable MySQL mode.');
@@ -0,0 +1,28 @@
1
+ /**
2
+ * DynamoDB connection factory.
3
+ *
4
+ * Dynamically imports @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb
5
+ * so these are optional peerDependencies (matching the pg/mysql2 pattern).
6
+ */
7
+ export interface DynamoDBConfig {
8
+ region?: string;
9
+ endpoint?: string;
10
+ [key: string]: unknown;
11
+ }
12
+ export type DocumentClient = {
13
+ send(command: unknown): Promise<unknown>;
14
+ };
15
+ export type DocumentClientConstructor = new (options: {
16
+ client: unknown;
17
+ }) => DocumentClient;
18
+ export type DynamoDBClientConstructor = new (options: unknown) => unknown;
19
+ /**
20
+ * Create a DynamoDBDocumentClient from the given config.
21
+ * Uses dynamic import so @aws-sdk/* are optional peer deps.
22
+ */
23
+ export declare function createDocumentClient(dbConfig: DynamoDBConfig): Promise<DocumentClient>;
24
+ /**
25
+ * Nullify the document client reference (DynamoDB connections are HTTP-based
26
+ * and stateless — no explicit pool close needed, but we clear the reference).
27
+ */
28
+ export declare function destroyDocumentClient(_client: DocumentClient | null): null;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * DynamoDB connection factory.
3
+ *
4
+ * Dynamically imports @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb
5
+ * so these are optional peerDependencies (matching the pg/mysql2 pattern).
6
+ */
7
+ /**
8
+ * Create a DynamoDBDocumentClient from the given config.
9
+ * Uses dynamic import so @aws-sdk/* are optional peer deps.
10
+ */
11
+ export async function createDocumentClient(dbConfig) {
12
+ const { DynamoDB } = await import('@aws-sdk/client-dynamodb');
13
+ const { DynamoDBDocument } = await import('@aws-sdk/lib-dynamodb');
14
+ const clientOptions = {};
15
+ if (dbConfig.region)
16
+ clientOptions.region = dbConfig.region;
17
+ if (dbConfig.endpoint)
18
+ clientOptions.endpoint = dbConfig.endpoint;
19
+ const rawClient = new DynamoDB(clientOptions);
20
+ return new DynamoDBDocument({ client: rawClient });
21
+ }
22
+ /**
23
+ * Nullify the document client reference (DynamoDB connections are HTTP-based
24
+ * and stateless — no explicit pool close needed, but we clear the reference).
25
+ */
26
+ export function destroyDocumentClient(_client) {
27
+ return null;
28
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * DynamoDB driver implementing the SqlDb PAL contract.
3
+ *
4
+ * Drop-in replacement for PostgresDB / MysqlDB — zero ORM core changes.
5
+ * Selected via config.orm.dynamodb.
6
+ */
7
+ import { createDocumentClient, destroyDocumentClient } from './connection.js';
8
+ import type { DocumentClient, DynamoDBConfig } from './connection.js';
9
+ import { buildPutItem, buildGetItem, buildUpdateItem, buildDeleteItem, buildScan, buildQuery } from './operation-builder.js';
10
+ import { introspectModels, getTopologicalOrder } from '../postgres/schema-introspector.js';
11
+ import { getDynamoKeyType } from './type-map.js';
12
+ import { store } from '@stonyx/orm';
13
+ import { createRecord } from '../manage-record.js';
14
+ import { getPluralName } from '../plural-registry.js';
15
+ import config from 'stonyx/config';
16
+ import log from 'stonyx/log';
17
+ import type { OrmRecord } from '../types/orm-types.js';
18
+ /**
19
+ * Load the DynamoDB DocumentClient command constructors via dynamic import.
20
+ * Returns a frozen object so it can be cached in deps.
21
+ */
22
+ export declare function loadDocClientCommands(): Promise<{
23
+ PutCommand: new (params: unknown) => unknown;
24
+ GetCommand: new (params: unknown) => unknown;
25
+ UpdateCommand: new (params: unknown) => unknown;
26
+ DeleteCommand: new (params: unknown) => unknown;
27
+ ScanCommand: new (params: unknown) => unknown;
28
+ QueryCommand: new (params: unknown) => unknown;
29
+ }>;
30
+ export declare function loadTableCommands(): Promise<{
31
+ DynamoDBClient: new (opts: unknown) => {
32
+ send(cmd: unknown): Promise<unknown>;
33
+ };
34
+ DescribeTableCommand: new (params: unknown) => unknown;
35
+ CreateTableCommand: new (params: unknown) => unknown;
36
+ UpdateTableCommand: new (params: unknown) => unknown;
37
+ }>;
38
+ interface PersistContext {
39
+ record?: OrmRecord;
40
+ recordId?: unknown;
41
+ oldState?: Record<string, unknown>;
42
+ rawData?: Record<string, unknown>;
43
+ }
44
+ interface PersistResponse {
45
+ data?: {
46
+ id?: unknown;
47
+ };
48
+ }
49
+ /** Minimal Orm module shape needed at runtime — avoids circular import at top-level. */
50
+ interface OrmModule {
51
+ default: {
52
+ instance: {
53
+ getRecordClasses(name: string): {
54
+ modelClass: {
55
+ memory?: boolean;
56
+ };
57
+ };
58
+ isView?(name: string): boolean;
59
+ };
60
+ };
61
+ }
62
+ export interface DynamoDBDeps {
63
+ createDocumentClient: typeof createDocumentClient;
64
+ destroyDocumentClient: typeof destroyDocumentClient;
65
+ loadDocClientCommands: typeof loadDocClientCommands;
66
+ loadTableCommands: typeof loadTableCommands;
67
+ buildPutItem: typeof buildPutItem;
68
+ buildGetItem: typeof buildGetItem;
69
+ buildUpdateItem: typeof buildUpdateItem;
70
+ buildDeleteItem: typeof buildDeleteItem;
71
+ buildScan: typeof buildScan;
72
+ buildQuery: typeof buildQuery;
73
+ introspectModels: typeof introspectModels;
74
+ getTopologicalOrder: typeof getTopologicalOrder;
75
+ getDynamoKeyType: typeof getDynamoKeyType;
76
+ createRecord: typeof createRecord;
77
+ store: typeof store;
78
+ getPluralName: typeof getPluralName;
79
+ config: typeof config;
80
+ log: typeof log;
81
+ /** Injected for testing — import('@stonyx/orm') replacement */
82
+ _importOrm?: () => Promise<OrmModule>;
83
+ [key: string]: unknown;
84
+ }
85
+ export default class DynamoDBDB {
86
+ static instance: DynamoDBDB | undefined;
87
+ deps: DynamoDBDeps;
88
+ client: DocumentClient | null;
89
+ dbConfig: DynamoDBConfig;
90
+ /** GSI registry built during init from model introspection. */
91
+ private _gsiRegistry;
92
+ constructor(deps?: Partial<DynamoDBDeps>);
93
+ private requireClient;
94
+ /** Resolve Orm singleton — falls back to real import in production. */
95
+ private _getOrm;
96
+ init(): Promise<void>;
97
+ /**
98
+ * For each model, DescribeTable — CreateTable if missing (with GSIs, PAY_PER_REQUEST).
99
+ * For existing tables, check for missing GSIs and UpdateTable + poll for ACTIVE.
100
+ */
101
+ startup(): Promise<void>;
102
+ shutdown(): Promise<void>;
103
+ persist(operation: string, modelName: string, context: PersistContext, response: PersistResponse): Promise<void>;
104
+ findRecord(modelName: string, id: unknown): Promise<OrmRecord | undefined>;
105
+ findAll(modelName: string, conditions?: Record<string, unknown>): Promise<OrmRecord[]>;
106
+ loadMemoryRecords(): Promise<void>;
107
+ private _persistCreate;
108
+ private _persistUpdate;
109
+ private _persistDelete;
110
+ private _paginatedScan;
111
+ private _paginatedQuery;
112
+ /**
113
+ * Build the GSI registry from model introspection.
114
+ * Registry: modelName → attrName → gsiName
115
+ *
116
+ * FK columns (belonging to belongsTo relationships) get a GSI automatically.
117
+ */
118
+ private _buildGsiRegistry;
119
+ /**
120
+ * Find a GSI that can serve the given conditions.
121
+ */
122
+ private _findGsiMatch;
123
+ private _buildAttributeDefinitions;
124
+ private _buildGsiDefinitions;
125
+ private _waitForTableActive;
126
+ private _itemToRawData;
127
+ private _recordToItem;
128
+ private _evictIfNotMemory;
129
+ loadAllRecords(): Promise<void>;
130
+ }
131
+ export {};