@casekit/orm 0.0.1-alpha.14 → 0.0.1-alpha.16
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/lib/orm.query.test.d.ts +2 -0
- package/lib/orm.query.test.d.ts.map +1 -0
- package/lib/orm.query.test.js +22 -0
- package/lib/orm.query.test.js.map +1 -0
- package/package.json +3 -6
- package/src/Connection.ts +65 -0
- package/src/errors.ts +18 -0
- package/src/index.ts +16 -0
- package/src/logger.ts +3 -0
- package/src/migrate/commands/implode.ts +46 -0
- package/src/migrate/index.ts +1 -0
- package/src/migrate/migrator.ts +24 -0
- package/src/migrate/sql/createExtensionsSql.test.ts +26 -0
- package/src/migrate/sql/createExtensionsSql.ts +16 -0
- package/src/migrate/sql/createForeignKeyConstraintSql.test.ts +50 -0
- package/src/migrate/sql/createForeignKeyConstraintSql.ts +44 -0
- package/src/migrate/sql/createSchemasSql.test.ts +81 -0
- package/src/migrate/sql/createSchemasSql.ts +15 -0
- package/src/migrate/sql/createTableSql.properties.ts +38 -0
- package/src/migrate/sql/createTableSql.test.ts +74 -0
- package/src/migrate/sql/createTableSql.ts +53 -0
- package/src/migrate/sql/createUniqueConstraintSql.ts +27 -0
- package/src/migrate/sql/dropSchemasSql.ts +15 -0
- package/src/migrate/sql/dropTableSql.ts +13 -0
- package/src/orm.query.test.ts +28 -0
- package/src/orm.ts +370 -0
- package/src/pull/index.ts +1 -0
- package/src/pull/introspect/getForeignKeys.ts +64 -0
- package/src/pull/introspect/getPrimaryKeys.ts +26 -0
- package/src/pull/introspect/getTables.ts +51 -0
- package/src/pull/introspect/getUniqueConstraints.ts +39 -0
- package/src/pull/parse/parseCreateUniqueIndexStatement.test.ts +14 -0
- package/src/pull/parse/parseCreateUniqueIndexStatement.ts +19 -0
- package/src/pull/pull.ts +78 -0
- package/src/pull/render/renderModel.test.ts +144 -0
- package/src/pull/render/renderModel.ts +141 -0
- package/src/pull/render/renderModelsIndex.ts +24 -0
- package/src/pull/render/renderRelations.ts +77 -0
- package/src/pull/render/renderRelationsIndex.ts +24 -0
- package/src/pull/types/ColumnMeta.ts +10 -0
- package/src/pull/types/ForeignKey.ts +6 -0
- package/src/pull/types/PrimaryKey.ts +6 -0
- package/src/pull/types/UniqueConstraint.ts +8 -0
- package/src/pull/util/format.ts +17 -0
- package/src/pull/util/quote.ts +7 -0
- package/src/pull/util/unquote.ts +9 -0
- package/src/queries/clauses/IncludeClause.ts +39 -0
- package/src/queries/clauses/LateralByClause.ts +4 -0
- package/src/queries/clauses/ReturningClause.ts +7 -0
- package/src/queries/clauses/SelectClause.ts +7 -0
- package/src/queries/clauses/WhereClause.ts +16 -0
- package/src/queries/clauses/helpers/OptionalColumn.ts +18 -0
- package/src/queries/clauses/helpers/OptionalParams.ts +14 -0
- package/src/queries/clauses/helpers/RequiredColumn.ts +8 -0
- package/src/queries/clauses/helpers/RequiredParams.ts +14 -0
- package/src/queries/clauses/include/IncludedRelationModel.ts +13 -0
- package/src/queries/clauses/include/IncludedRelationName.ts +11 -0
- package/src/queries/clauses/include/IncludedRelationQuery.ts +20 -0
- package/src/queries/clauses/where/buildWhereClause.ts +121 -0
- package/src/queries/clauses/where/buildWhereClauses.test.ts +145 -0
- package/src/queries/clauses/where/buildWhereClauses.ts +45 -0
- package/src/queries/clauses/where/operators.ts +13 -0
- package/src/queries/clauses/where/types/WhereClauseValue.ts +39 -0
- package/src/queries/count/buildCount.ts +77 -0
- package/src/queries/count/countToSql.ts +66 -0
- package/src/queries/count/tests/count.test.ts +35 -0
- package/src/queries/count/types/BaseCountParams.ts +11 -0
- package/src/queries/count/types/CountBuilder.ts +45 -0
- package/src/queries/count/types/CountParams.ts +27 -0
- package/src/queries/count.ts +33 -0
- package/src/queries/create/buildCreate.ts +67 -0
- package/src/queries/create/createResultSchema.ts +24 -0
- package/src/queries/create/createToSql.ts +44 -0
- package/src/queries/create/tests/createMany.varied-keys.test.ts +28 -0
- package/src/queries/create/tests/createOne.test-d.ts +116 -0
- package/src/queries/create/tests/createOne.test.ts +197 -0
- package/src/queries/create/types/BaseCreateManyParams.ts +7 -0
- package/src/queries/create/types/BaseCreateOneParams.ts +7 -0
- package/src/queries/create/types/CreateManyParams.ts +15 -0
- package/src/queries/create/types/CreateManyResult.ts +17 -0
- package/src/queries/create/types/CreateOneParams.ts +22 -0
- package/src/queries/create/types/CreateOneResult.ts +17 -0
- package/src/queries/createMany.ts +38 -0
- package/src/queries/createOne.ts +27 -0
- package/src/queries/delete/buildDelete.ts +56 -0
- package/src/queries/delete/deleteResultSchema.ts +23 -0
- package/src/queries/delete/deleteToSql.ts +48 -0
- package/src/queries/delete/tests/deleteOne.test.ts +108 -0
- package/src/queries/delete/types/BaseDeleteParams.ts +9 -0
- package/src/queries/delete/types/DeleteManyResult.ts +16 -0
- package/src/queries/delete/types/DeleteOneResult.ts +16 -0
- package/src/queries/delete/types/DeleteParams.ts +12 -0
- package/src/queries/deleteMany.ts +33 -0
- package/src/queries/deleteOne.ts +32 -0
- package/src/queries/find/buildFind.ts +138 -0
- package/src/queries/find/findResultSchema.ts +32 -0
- package/src/queries/find/findToSql.test.ts +123 -0
- package/src/queries/find/findToSql.ts +141 -0
- package/src/queries/find/getIncludedManyToManyRelations.ts +49 -0
- package/src/queries/find/getIncludedOneToManyRelations.ts +44 -0
- package/src/queries/find/tests/findMany.include.test.ts +107 -0
- package/src/queries/find/tests/findMany.limit.test-d.ts +75 -0
- package/src/queries/find/tests/findMany.limit.test.ts +176 -0
- package/src/queries/find/tests/findMany.nullable-relations.test.ts +127 -0
- package/src/queries/find/tests/findMany.orderBy.test-d.ts +84 -0
- package/src/queries/find/tests/findMany.orderBy.test.ts +184 -0
- package/src/queries/find/tests/findMany.select.test-d.ts +117 -0
- package/src/queries/find/tests/findMany.select.test.ts +188 -0
- package/src/queries/find/tests/findMany.too-deep.test-d.ts +154 -0
- package/src/queries/find/tests/findMany.where.test-d.ts +85 -0
- package/src/queries/find/tests/findMany.where.test.ts +76 -0
- package/src/queries/find/tests/middleware.find.where.test.ts +467 -0
- package/src/queries/find/types/BaseFindParams.ts +18 -0
- package/src/queries/find/types/FindBuilder.ts +73 -0
- package/src/queries/find/types/FindManyParams.ts +24 -0
- package/src/queries/find/types/FindManyResult.ts +13 -0
- package/src/queries/find/types/FindOneParams.ts +17 -0
- package/src/queries/find/types/FindOneResult.ts +50 -0
- package/src/queries/findMany.ts +134 -0
- package/src/queries/findOne.ts +30 -0
- package/src/queries/middleware/Middleware.ts +53 -0
- package/src/queries/middleware/ValuesMiddleware.ts +9 -0
- package/src/queries/middleware/WhereMiddleware.ts +16 -0
- package/src/queries/update/buildUpdate.ts +76 -0
- package/src/queries/update/tests/updateOne.test.ts +118 -0
- package/src/queries/update/types/BaseUpdateParams.ts +10 -0
- package/src/queries/update/types/UpdateManyResult.ts +16 -0
- package/src/queries/update/types/UpdateOneResult.ts +16 -0
- package/src/queries/update/types/UpdateParams.ts +14 -0
- package/src/queries/update/types/UpdateValues.ts +12 -0
- package/src/queries/update/updateResultSchema.ts +23 -0
- package/src/queries/update/updateToSql.ts +43 -0
- package/src/queries/updateMany.ts +33 -0
- package/src/queries/updateOne.ts +33 -0
- package/src/queries/util/hasConditions.test.ts +36 -0
- package/src/queries/util/hasConditions.ts +14 -0
- package/src/queries/util/rowToObject.test.ts +76 -0
- package/src/queries/util/rowToObject.ts +13 -0
- package/src/queries/util/tableAlias.test.ts +20 -0
- package/src/queries/util/tableAlias.ts +10 -0
- package/src/schema/populate/composeMiddleware.ts +51 -0
- package/src/schema/populate/populateConfiguration.ts +48 -0
- package/src/schema/populate/populateModel.ts +98 -0
- package/src/schema/populate/suggestedColumnSchema.ts +62 -0
- package/src/schema/types/base/BaseColumn.ts +10 -0
- package/src/schema/types/base/BaseConfiguration.ts +14 -0
- package/src/schema/types/base/BaseModel.ts +12 -0
- package/src/schema/types/base/BaseModels.ts +3 -0
- package/src/schema/types/base/BaseOrm.ts +8 -0
- package/src/schema/types/base/BaseRelation.ts +19 -0
- package/src/schema/types/base/BaseRelations.ts +3 -0
- package/src/schema/types/constraints/ForeignKey.ts +13 -0
- package/src/schema/types/constraints/UniqueConstraint.ts +9 -0
- package/src/schema/types/helpers/ColumnName.ts +6 -0
- package/src/schema/types/helpers/ColumnType.ts +23 -0
- package/src/schema/types/helpers/Columns.ts +3 -0
- package/src/schema/types/helpers/HasDefault.ts +11 -0
- package/src/schema/types/helpers/IsNullable.ts +7 -0
- package/src/schema/types/helpers/IsProvided.ts +8 -0
- package/src/schema/types/helpers/IsSerial.ts +10 -0
- package/src/schema/types/helpers/ModelName.ts +6 -0
- package/src/schema/types/loose/LooseColumnDefinition.ts +80 -0
- package/src/schema/types/loose/LooseModelDefinition.ts +47 -0
- package/src/schema/types/loose/LooseModelDefinitions.ts +3 -0
- package/src/schema/types/loose/LooseRelationDefinition.ts +10 -0
- package/src/schema/types/loose/LooseRelationsDefinition.ts +8 -0
- package/src/schema/types/loose/LooseRelationsDefinitions.ts +7 -0
- package/src/schema/types/postgres/DataType.ts +65 -0
- package/src/schema/types/relations/ManyToManyRelation.ts +15 -0
- package/src/schema/types/relations/ManyToOneRelation.ts +14 -0
- package/src/schema/types/relations/OneToManyRelation.ts +12 -0
- package/src/schema/types/strict/ColumnDefinition.ts +81 -0
- package/src/schema/types/strict/ModelDefinition.ts +47 -0
- package/src/schema/types/strict/ModelDefinitions.ts +3 -0
- package/src/schema/types/strict/RelationDefinition.ts +13 -0
- package/src/schema/types/strict/RelationsDefinition.ts +8 -0
- package/src/schema/types/strict/RelationsDefinitions.ts +7 -0
- package/src/schema/validate/validateConfiguration.ts +9 -0
- package/src/schema/validate/validateModel.ts +32 -0
- package/src/sql/SQLStatement.test.ts +112 -0
- package/src/sql/SQLStatement.ts +86 -0
- package/src/sql/index.ts +2 -0
- package/src/sql/sql.ts +47 -0
- package/src/test/db/index.ts +27 -0
- package/src/test/db/models/foo.model.ts +24 -0
- package/src/test/db/models/foo.relations.ts +4 -0
- package/src/test/db/models/post.model.ts +34 -0
- package/src/test/db/models/post.relations.ts +21 -0
- package/src/test/db/models/tenant.model.ts +14 -0
- package/src/test/db/models/tenant.relations.ts +22 -0
- package/src/test/db/models/tenantUser.model.ts +23 -0
- package/src/test/db/models/tenantUser.relations.ts +15 -0
- package/src/test/db/models/user.model.ts +29 -0
- package/src/test/db/models/user.relations.ts +38 -0
- package/src/test/db/models.ts +15 -0
- package/src/test/db/relations.ts +13 -0
- package/src/test/gen/column.ts +39 -0
- package/src/test/gen/index.ts +2 -0
- package/src/test/gen/model.ts +58 -0
- package/src/test/gen/sqldate.ts +8 -0
- package/src/test/globalSetup.ts +6 -0
- package/src/test/seed/index.ts +75 -0
- package/src/test/util/withRollback.ts +18 -0
- package/src/test/util/withTransaction.ts +19 -0
- package/src/types/ColumnName.ts +6 -0
- package/src/types/ColumnType.ts +20 -0
- package/src/types/Configuration.ts +21 -0
- package/src/types/ModelType.ts +20 -0
- package/src/types/util/DeepRequired.ts +7 -0
- package/src/types/util/DisallowExtraKeys.ts +5 -0
- package/src/types/util/NonEmptyArray.ts +1 -0
- package/src/types/util/Simplify.ts +8 -0
- package/src/util/ensureArray.ts +4 -0
- package/src/util/interleave.test.ts +35 -0
- package/src/util/interleave.ts +8 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orm.query.test.d.ts","sourceRoot":"","sources":["../src/orm.query.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { db } from "./test/db";
|
|
3
|
+
import { seed } from "./test/seed";
|
|
4
|
+
describe("orm.query", () => {
|
|
5
|
+
test("allows arbitrary sql queries to be run against the database", async () => {
|
|
6
|
+
await db.transact(async (db) => {
|
|
7
|
+
await seed(db, {
|
|
8
|
+
users: [
|
|
9
|
+
{
|
|
10
|
+
username: "Russell",
|
|
11
|
+
tenants: [{ name: "Popova Park", posts: 6 }],
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
const result = await db.query `
|
|
16
|
+
select count(1)::int as total from casekit.post;
|
|
17
|
+
`;
|
|
18
|
+
expect(result[0]).toEqual({ total: 6 });
|
|
19
|
+
}, { rollback: true });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=orm.query.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orm.query.test.js","sourceRoot":"","sources":["../src/orm.query.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACvB,IAAI,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,EAAE,CAAC,QAAQ,CACb,KAAK,EAAE,EAAE,EAAE,EAAE;YACT,MAAM,IAAI,CAAC,EAAE,EAAE;gBACX,KAAK,EAAE;oBACH;wBACI,QAAQ,EAAE,SAAS;wBACnB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;qBAC/C;iBACJ;aACJ,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAmB;;iBAE/C,CAAC;YAEF,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5C,CAAC,EACD,EAAE,QAAQ,EAAE,IAAI,EAAE,CACrB,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@casekit/orm",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.16",
|
|
4
4
|
"description": "A simple ORM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -9,11 +9,8 @@
|
|
|
9
9
|
".": "./lib/index.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"./
|
|
13
|
-
"
|
|
14
|
-
"!./lib/**.test.js.map",
|
|
15
|
-
"!./lib/**.test.d.ts",
|
|
16
|
-
"!./lib/**.test.d.ts.map"
|
|
12
|
+
"./src/*",
|
|
13
|
+
"./lib/*"
|
|
17
14
|
],
|
|
18
15
|
"keywords": [],
|
|
19
16
|
"author": "",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import pg, { QueryResultRow } from "pg";
|
|
2
|
+
import pgfmt from "pg-format";
|
|
3
|
+
import * as uuid from "uuid";
|
|
4
|
+
|
|
5
|
+
import { SQLStatement } from "./sql";
|
|
6
|
+
|
|
7
|
+
export class Connection {
|
|
8
|
+
constructor(
|
|
9
|
+
private pool: pg.Pool,
|
|
10
|
+
private client?: pg.PoolClient,
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
public query = async <T extends QueryResultRow>(query: SQLStatement) => {
|
|
14
|
+
if (this.client) {
|
|
15
|
+
return this.client.query<T>(query);
|
|
16
|
+
} else {
|
|
17
|
+
return this.pool.query(query);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
public transact = async <T>(
|
|
22
|
+
fn: (conn: Connection) => Promise<T>,
|
|
23
|
+
{ rollback = false }: { rollback?: boolean } = {},
|
|
24
|
+
) => {
|
|
25
|
+
if (this.client) {
|
|
26
|
+
const savepoint = uuid.v4();
|
|
27
|
+
try {
|
|
28
|
+
await this.client.query(pgfmt("SAVEPOINT %I", savepoint));
|
|
29
|
+
const result = await fn(this);
|
|
30
|
+
if (rollback) {
|
|
31
|
+
await this.client.query(
|
|
32
|
+
pgfmt("ROLLBACK TO SAVEPOINT %I", savepoint),
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
await this.client.query(
|
|
36
|
+
pgfmt("RELEASE SAVEPOINT %I", savepoint),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
await this.client.query(
|
|
42
|
+
pgfmt("ROLLBACK TO SAVEPOINT %I", savepoint),
|
|
43
|
+
);
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const client = await this.pool.connect();
|
|
48
|
+
try {
|
|
49
|
+
await client.query("BEGIN");
|
|
50
|
+
const result = await fn(new Connection(this.pool, client));
|
|
51
|
+
if (rollback) {
|
|
52
|
+
await client.query("ROLLBACK");
|
|
53
|
+
} else {
|
|
54
|
+
await client.query("COMMIT");
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
await client.query("ROLLBACK");
|
|
59
|
+
throw e;
|
|
60
|
+
} finally {
|
|
61
|
+
client.release();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseModel } from "./schema/types/base/BaseModel";
|
|
2
|
+
|
|
3
|
+
type OrmErrorMeta = {
|
|
4
|
+
model?: [string, BaseModel];
|
|
5
|
+
data?: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class OrmError extends Error {
|
|
9
|
+
meta: OrmErrorMeta | undefined;
|
|
10
|
+
|
|
11
|
+
constructor(message: string, meta?: OrmErrorMeta) {
|
|
12
|
+
super();
|
|
13
|
+
this.message = message;
|
|
14
|
+
this.meta = meta;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class InvalidModelDefinitionError extends OrmError {}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { OrmError } from "./errors";
|
|
2
|
+
export { migrator, type Migrator } from "./migrate";
|
|
3
|
+
export { Orm, orm } from "./orm";
|
|
4
|
+
export { pull } from "./pull";
|
|
5
|
+
export * from "./queries/clauses/where/operators";
|
|
6
|
+
export { type Middleware } from "./queries/middleware/Middleware";
|
|
7
|
+
/**
|
|
8
|
+
* TODO find a way to use the strict definitions here
|
|
9
|
+
* without destroying VS Code autocomplete performance
|
|
10
|
+
*/
|
|
11
|
+
export { type LooseModelDefinition as ModelDefinition } from "./schema/types/loose/LooseModelDefinition";
|
|
12
|
+
export { type LooseRelationsDefinition as RelationsDefinition } from "./schema/types/loose/LooseRelationsDefinition";
|
|
13
|
+
export { SQLStatement, sql } from "./sql";
|
|
14
|
+
export type { ColumnName } from "./types/ColumnName";
|
|
15
|
+
export type { ColumnType } from "./types/ColumnType";
|
|
16
|
+
export type { ModelType } from "./types/ModelType";
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BaseOrm } from "../../schema/types/base/BaseOrm";
|
|
2
|
+
import { SQLStatement } from "../../sql";
|
|
3
|
+
import { createExtensionsSql } from "../sql/createExtensionsSql";
|
|
4
|
+
import { createForeignKeyConstraintSql } from "../sql/createForeignKeyConstraintSql";
|
|
5
|
+
import { createSchemasSql } from "../sql/createSchemasSql";
|
|
6
|
+
import { createTableSql } from "../sql/createTableSql";
|
|
7
|
+
import { createUniqueConstraintSql } from "../sql/createUniqueConstraintSql";
|
|
8
|
+
import { dropSchemasSql } from "../sql/dropSchemasSql";
|
|
9
|
+
|
|
10
|
+
export const implode = async (
|
|
11
|
+
db: BaseOrm,
|
|
12
|
+
{ dryRun, output }: { dryRun: boolean; output: boolean },
|
|
13
|
+
) => {
|
|
14
|
+
const statement = new SQLStatement();
|
|
15
|
+
|
|
16
|
+
statement.push(dropSchemasSql(db), "\n");
|
|
17
|
+
statement.push(createSchemasSql(db), "\n");
|
|
18
|
+
statement.push(createExtensionsSql(db), "\n");
|
|
19
|
+
|
|
20
|
+
for (const model of Object.values(db.models)) {
|
|
21
|
+
statement.push(createTableSql(model), "\n");
|
|
22
|
+
for (const constraint of model.uniqueConstraints) {
|
|
23
|
+
statement.push(createUniqueConstraintSql(model, constraint), "\n");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const model of Object.values(db.models)) {
|
|
28
|
+
for (const foreignKey of model.foreignKeys) {
|
|
29
|
+
statement.push(
|
|
30
|
+
createForeignKeyConstraintSql(model, foreignKey),
|
|
31
|
+
"\n",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (output || process.env.ORM_VERBOSE_LOGGING) console.log(statement.text);
|
|
37
|
+
|
|
38
|
+
if (!dryRun) {
|
|
39
|
+
try {
|
|
40
|
+
await db.connection.query(statement);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.log(e);
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type Migrator, migrator } from "./migrator";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Orm } from "..";
|
|
2
|
+
import { BaseOrm } from "../schema/types/base/BaseOrm";
|
|
3
|
+
import { LooseModelDefinitions } from "../schema/types/loose/LooseModelDefinitions";
|
|
4
|
+
import { LooseRelationsDefinitions } from "../schema/types/loose/LooseRelationsDefinitions";
|
|
5
|
+
import { implode } from "./commands/implode";
|
|
6
|
+
|
|
7
|
+
export class Migrator {
|
|
8
|
+
public db: BaseOrm;
|
|
9
|
+
|
|
10
|
+
constructor(db: BaseOrm) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public async implode(opts = { dryRun: true, output: true }) {
|
|
15
|
+
await implode(this.db, opts);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const migrator = <
|
|
20
|
+
Models extends LooseModelDefinitions,
|
|
21
|
+
Relations extends LooseRelationsDefinitions<Models>,
|
|
22
|
+
>(
|
|
23
|
+
db: Orm<Models, Relations>,
|
|
24
|
+
) => new Migrator(db as BaseOrm);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { unindent } from "@casekit/unindent";
|
|
2
|
+
|
|
3
|
+
import pg from "pg";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { orm } from "../../orm";
|
|
7
|
+
import { createExtensionsSql } from "./createExtensionsSql";
|
|
8
|
+
|
|
9
|
+
describe("createExtensionsSql", () => {
|
|
10
|
+
test("it generates a CREATE EXTENSION command for each extension", () => {
|
|
11
|
+
const db = orm({
|
|
12
|
+
extensions: ["uuid-ossp", "postgis"],
|
|
13
|
+
models: {},
|
|
14
|
+
pool: new pg.Pool(),
|
|
15
|
+
});
|
|
16
|
+
expect(createExtensionsSql(db)?.text).toEqual(unindent`
|
|
17
|
+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
18
|
+
CREATE EXTENSION IF NOT EXISTS postgis;
|
|
19
|
+
`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("if no extensions are specified, it returns null", () => {
|
|
23
|
+
const db = orm({ models: {}, pool: new pg.Pool() });
|
|
24
|
+
expect(createExtensionsSql(db)).toEqual(null);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pgfmt from "pg-format";
|
|
2
|
+
|
|
3
|
+
import { BaseOrm } from "../../schema/types/base/BaseOrm";
|
|
4
|
+
import { SQLStatement } from "../../sql";
|
|
5
|
+
|
|
6
|
+
export const createExtensionsSql = (db: BaseOrm): SQLStatement | null => {
|
|
7
|
+
if (db.config.extensions?.length === 0) return null;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
new SQLStatement(
|
|
11
|
+
db.config.extensions
|
|
12
|
+
?.map((e) => pgfmt("CREATE EXTENSION IF NOT EXISTS %I;", e))
|
|
13
|
+
.join("\n"),
|
|
14
|
+
) ?? null
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { unindent } from "@casekit/unindent";
|
|
2
|
+
|
|
3
|
+
import pg from "pg";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { ModelDefinition, orm, sql } from "../../";
|
|
7
|
+
import { createForeignKeyConstraintSql } from "./createForeignKeyConstraintSql";
|
|
8
|
+
|
|
9
|
+
describe("createForeignKeyConstraintSql", () => {
|
|
10
|
+
test("it generates an ALTER TABLE command for the specified foreign key constraint", () => {
|
|
11
|
+
const foo = {
|
|
12
|
+
schema: "casekit",
|
|
13
|
+
columns: {
|
|
14
|
+
id: { type: "uuid" },
|
|
15
|
+
barId: {
|
|
16
|
+
name: "bar_id",
|
|
17
|
+
type: "uuid",
|
|
18
|
+
references: { table: "bar", column: "id" },
|
|
19
|
+
},
|
|
20
|
+
bazId: {
|
|
21
|
+
name: "baz_id",
|
|
22
|
+
type: "uuid",
|
|
23
|
+
references: { table: "baz", column: "id" },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
foreignKeys: [
|
|
27
|
+
{
|
|
28
|
+
columns: ["bar_id", "baz_id"],
|
|
29
|
+
references: {
|
|
30
|
+
table: "quux",
|
|
31
|
+
columns: ["bar_id", "baz_id"],
|
|
32
|
+
},
|
|
33
|
+
onUpdate: sql`CASCADE`,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
} as const satisfies ModelDefinition;
|
|
37
|
+
|
|
38
|
+
const db = orm({ models: { foo }, pool: new pg.Pool() });
|
|
39
|
+
|
|
40
|
+
const result = db.models.foo.foreignKeys
|
|
41
|
+
.map((fk) => createForeignKeyConstraintSql(db.models.foo, fk).text)
|
|
42
|
+
.join("\n");
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual(unindent`
|
|
45
|
+
ALTER TABLE casekit.foo ADD CONSTRAINT foo_bar_id_baz_id_fkey FOREIGN KEY (bar_id, baz_id) REFERENCES casekit.quux (bar_id, baz_id) ON UPDATE CASCADE;
|
|
46
|
+
ALTER TABLE casekit.foo ADD CONSTRAINT foo_bar_id_fkey FOREIGN KEY (bar_id) REFERENCES casekit.bar (id);
|
|
47
|
+
ALTER TABLE casekit.foo ADD CONSTRAINT foo_baz_id_fkey FOREIGN KEY (baz_id) REFERENCES casekit.baz (id);
|
|
48
|
+
`);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import pgfmt from "pg-format";
|
|
2
|
+
|
|
3
|
+
import { SQLStatement } from "../..";
|
|
4
|
+
import { BaseModel } from "../../schema/types/base/BaseModel";
|
|
5
|
+
import { ForeignKey } from "../../schema/types/constraints/ForeignKey";
|
|
6
|
+
|
|
7
|
+
export const createForeignKeyConstraintSql = (
|
|
8
|
+
model: BaseModel,
|
|
9
|
+
constraint: ForeignKey,
|
|
10
|
+
) => {
|
|
11
|
+
const statement = new SQLStatement();
|
|
12
|
+
statement.push(
|
|
13
|
+
pgfmt(`ALTER TABLE %I.%I ADD CONSTRAINT `, model.schema, model.table),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
statement.push(
|
|
17
|
+
pgfmt("%I", [model.table, ...constraint.columns, "fkey"].join("_")),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
statement.push(pgfmt(` FOREIGN KEY (`, model.schema, model.table));
|
|
21
|
+
statement.push(constraint.columns.map((c) => pgfmt("%I", c)).join(", "));
|
|
22
|
+
statement.push(")");
|
|
23
|
+
statement.push(
|
|
24
|
+
pgfmt(
|
|
25
|
+
` REFERENCES %I.%I (`,
|
|
26
|
+
constraint.references.schema,
|
|
27
|
+
constraint.references.table,
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
statement.push(
|
|
31
|
+
constraint.references.columns.map((c) => pgfmt("%I", c)).join(", "),
|
|
32
|
+
);
|
|
33
|
+
statement.push(")");
|
|
34
|
+
if (constraint.onDelete) {
|
|
35
|
+
statement.push(` ON DELETE `);
|
|
36
|
+
statement.push(constraint.onDelete);
|
|
37
|
+
}
|
|
38
|
+
if (constraint.onUpdate) {
|
|
39
|
+
statement.push(` ON UPDATE `);
|
|
40
|
+
statement.push(constraint.onUpdate);
|
|
41
|
+
}
|
|
42
|
+
statement.push(";");
|
|
43
|
+
return statement;
|
|
44
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { unindent } from "@casekit/unindent";
|
|
2
|
+
|
|
3
|
+
import pg from "pg";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { ModelDefinition, orm } from "../../";
|
|
8
|
+
import { createSchemasSql } from "./createSchemasSql";
|
|
9
|
+
|
|
10
|
+
describe("createSchemaSql", () => {
|
|
11
|
+
test("it generates a CREATE SCHEMA command for each unique schema used", () => {
|
|
12
|
+
const a = {
|
|
13
|
+
schema: "foo",
|
|
14
|
+
columns: {
|
|
15
|
+
id: { type: "uuid", zodSchema: z.string() },
|
|
16
|
+
},
|
|
17
|
+
} as const satisfies ModelDefinition;
|
|
18
|
+
|
|
19
|
+
const b = {
|
|
20
|
+
schema: "bar",
|
|
21
|
+
columns: {
|
|
22
|
+
id: { type: "uuid", zodSchema: z.string() },
|
|
23
|
+
},
|
|
24
|
+
} as const satisfies ModelDefinition;
|
|
25
|
+
|
|
26
|
+
const db = orm({ models: { a, b }, pool: new pg.Pool() });
|
|
27
|
+
expect(createSchemasSql(db).text).toEqual(unindent`
|
|
28
|
+
CREATE SCHEMA IF NOT EXISTS foo;
|
|
29
|
+
CREATE SCHEMA IF NOT EXISTS bar;
|
|
30
|
+
`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("it pulls schema from the config if not specified on the model", () => {
|
|
34
|
+
const a = {
|
|
35
|
+
columns: {
|
|
36
|
+
id: { type: "uuid", zodSchema: z.string() },
|
|
37
|
+
},
|
|
38
|
+
} as const satisfies ModelDefinition;
|
|
39
|
+
|
|
40
|
+
const b = {
|
|
41
|
+
schema: "bar",
|
|
42
|
+
columns: {
|
|
43
|
+
id: { type: "uuid", zodSchema: z.string() },
|
|
44
|
+
},
|
|
45
|
+
} as const satisfies ModelDefinition;
|
|
46
|
+
|
|
47
|
+
const db = orm({
|
|
48
|
+
schema: "foo",
|
|
49
|
+
models: { a, b },
|
|
50
|
+
pool: new pg.Pool(),
|
|
51
|
+
});
|
|
52
|
+
expect(createSchemasSql(db).text).toEqual(unindent`
|
|
53
|
+
CREATE SCHEMA IF NOT EXISTS foo;
|
|
54
|
+
CREATE SCHEMA IF NOT EXISTS bar;
|
|
55
|
+
`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("if no schema is specified at all, it tries to create the public schema", () => {
|
|
59
|
+
const a = {
|
|
60
|
+
columns: {
|
|
61
|
+
id: { type: "uuid", zodSchema: z.string() },
|
|
62
|
+
},
|
|
63
|
+
} as const satisfies ModelDefinition;
|
|
64
|
+
|
|
65
|
+
const b = {
|
|
66
|
+
schema: "bar",
|
|
67
|
+
columns: {
|
|
68
|
+
id: { type: "uuid", zodSchema: z.string() },
|
|
69
|
+
},
|
|
70
|
+
} as const satisfies ModelDefinition;
|
|
71
|
+
|
|
72
|
+
const db = orm({
|
|
73
|
+
models: { a, b },
|
|
74
|
+
pool: new pg.Pool(),
|
|
75
|
+
});
|
|
76
|
+
expect(createSchemasSql(db).text).toEqual(unindent`
|
|
77
|
+
CREATE SCHEMA IF NOT EXISTS public;
|
|
78
|
+
CREATE SCHEMA IF NOT EXISTS bar;
|
|
79
|
+
`);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { uniq } from "lodash-es";
|
|
2
|
+
import pgfmt from "pg-format";
|
|
3
|
+
|
|
4
|
+
import { BaseOrm } from "../../schema/types/base/BaseOrm";
|
|
5
|
+
import { SQLStatement } from "../../sql";
|
|
6
|
+
|
|
7
|
+
export const createSchemasSql = (db: BaseOrm): SQLStatement => {
|
|
8
|
+
const schemas = uniq(Object.values(db.models).map((m) => m.schema));
|
|
9
|
+
|
|
10
|
+
return new SQLStatement(
|
|
11
|
+
schemas
|
|
12
|
+
.map((s) => pgfmt("CREATE SCHEMA IF NOT EXISTS %I;", s))
|
|
13
|
+
.join("\n"),
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { test } from "@fast-check/vitest";
|
|
2
|
+
import { isEqual } from "lodash-es";
|
|
3
|
+
import pg from "pg";
|
|
4
|
+
import pgfmt from "pg-format";
|
|
5
|
+
|
|
6
|
+
import { orm } from "../..";
|
|
7
|
+
import * as gen from "../../test/gen";
|
|
8
|
+
import { withRollback } from "../../test/util/withRollback";
|
|
9
|
+
import { createSchemasSql } from "./createSchemasSql";
|
|
10
|
+
import { createTableSql } from "./createTableSql";
|
|
11
|
+
|
|
12
|
+
test.prop([gen.model()])("should generate valid DDL", async (model) => {
|
|
13
|
+
return await withRollback(async (client) => {
|
|
14
|
+
const db = orm({
|
|
15
|
+
models: { model },
|
|
16
|
+
relations: { model: {} },
|
|
17
|
+
|
|
18
|
+
pool: new pg.Pool(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// create the schema
|
|
22
|
+
await client.query(createSchemasSql(db));
|
|
23
|
+
|
|
24
|
+
// create the table
|
|
25
|
+
await client.query(createTableSql(db.models.model));
|
|
26
|
+
|
|
27
|
+
// select from the newly created table so we can check it's there
|
|
28
|
+
const result = await client.query(
|
|
29
|
+
pgfmt("select * from %I.%I", model.schema, model.table),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// the selected fields should match the ones defined in the model
|
|
33
|
+
return isEqual(
|
|
34
|
+
result.fields.map((f) => f.name),
|
|
35
|
+
Object.values(model.columns).map((c) => c.name),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { unindent } from "@casekit/unindent";
|
|
2
|
+
|
|
3
|
+
import { snakeCase, uniqueId } from "lodash-es";
|
|
4
|
+
import pg from "pg";
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import { ModelDefinition, orm } from "../..";
|
|
9
|
+
import { sql } from "../../sql";
|
|
10
|
+
import { db } from "../../test/db";
|
|
11
|
+
import { createTableSql } from "./createTableSql";
|
|
12
|
+
|
|
13
|
+
describe("createTableSql", () => {
|
|
14
|
+
test("it generates a CREATE TABLE command", () => {
|
|
15
|
+
expect(createTableSql(db.models.user).text).toEqual(unindent`
|
|
16
|
+
CREATE TABLE casekit."user" (
|
|
17
|
+
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
|
18
|
+
username text NOT NULL,
|
|
19
|
+
invited_by_id uuid,
|
|
20
|
+
created_at timestamp,
|
|
21
|
+
updated_at timestamp,
|
|
22
|
+
deleted_at timestamp,
|
|
23
|
+
PRIMARY KEY (id)
|
|
24
|
+
);
|
|
25
|
+
`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("the generated DDL successfully creates a table", async () => {
|
|
29
|
+
const table = uniqueId("table-");
|
|
30
|
+
const post = {
|
|
31
|
+
table,
|
|
32
|
+
columns: {
|
|
33
|
+
id: {
|
|
34
|
+
zodSchema: z.string().uuid(),
|
|
35
|
+
type: "uuid",
|
|
36
|
+
default: sql`uuid_generate_v4()`,
|
|
37
|
+
},
|
|
38
|
+
title: {
|
|
39
|
+
zodSchema: z.string(),
|
|
40
|
+
type: "text",
|
|
41
|
+
default: "My first post",
|
|
42
|
+
},
|
|
43
|
+
content: { zodSchema: z.string(), type: "text" },
|
|
44
|
+
publishedAt: {
|
|
45
|
+
zodSchema: z.date(),
|
|
46
|
+
type: "timestamp",
|
|
47
|
+
nullable: true,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
} as const satisfies ModelDefinition;
|
|
51
|
+
await orm({
|
|
52
|
+
models: { post },
|
|
53
|
+
relations: { post: {} },
|
|
54
|
+
naming: { column: snakeCase },
|
|
55
|
+
pool: new pg.Pool(),
|
|
56
|
+
}).transact(
|
|
57
|
+
async (db) => {
|
|
58
|
+
await db.connection.query(createTableSql(db.models.post));
|
|
59
|
+
|
|
60
|
+
const result = await db.connection.query(
|
|
61
|
+
sql`select * from public.%I`.withIdentifiers(table),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(result.fields.map((f) => f.name)).toEqual([
|
|
65
|
+
"id",
|
|
66
|
+
"title",
|
|
67
|
+
"content",
|
|
68
|
+
"published_at",
|
|
69
|
+
]);
|
|
70
|
+
},
|
|
71
|
+
{ rollback: true },
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import pgfmt from "pg-format";
|
|
2
|
+
|
|
3
|
+
import { BaseModel } from "../../schema/types/base/BaseModel";
|
|
4
|
+
import { SQLStatement } from "../../sql";
|
|
5
|
+
import { interleave } from "../../util/interleave";
|
|
6
|
+
|
|
7
|
+
export const createTableSql = (model: BaseModel): SQLStatement => {
|
|
8
|
+
const columns = Object.values(model.columns);
|
|
9
|
+
const primaryKey = model.primaryKey;
|
|
10
|
+
|
|
11
|
+
const statement = new SQLStatement();
|
|
12
|
+
|
|
13
|
+
statement.push(
|
|
14
|
+
pgfmt("CREATE TABLE %I.%I (\n", model.schema ?? "public", model.table),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
statement.push(
|
|
18
|
+
...interleave(
|
|
19
|
+
columns.map((column) => {
|
|
20
|
+
const definition = new SQLStatement(
|
|
21
|
+
pgfmt(` %I %s`, column.name, column.type),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!column.nullable) definition.push(pgfmt(" NOT NULL"));
|
|
25
|
+
|
|
26
|
+
if (column.default instanceof SQLStatement) {
|
|
27
|
+
definition.push(" DEFAULT ");
|
|
28
|
+
definition.push(column.default);
|
|
29
|
+
} else if (
|
|
30
|
+
column.default !== null &&
|
|
31
|
+
column.default !== undefined
|
|
32
|
+
) {
|
|
33
|
+
definition.push(pgfmt(" DEFAULT %L", column.default));
|
|
34
|
+
}
|
|
35
|
+
return definition;
|
|
36
|
+
}),
|
|
37
|
+
",\n",
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (primaryKey.length > 0) {
|
|
42
|
+
statement.push(
|
|
43
|
+
pgfmt(
|
|
44
|
+
`,\n PRIMARY KEY (${primaryKey.map(() => "%I").join(", ")})`,
|
|
45
|
+
...primaryKey,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
statement.push(pgfmt("\n);"));
|
|
51
|
+
|
|
52
|
+
return statement;
|
|
53
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pgfmt from "pg-format";
|
|
2
|
+
|
|
3
|
+
import { SQLStatement } from "../..";
|
|
4
|
+
import { BaseModel } from "../../schema/types/base/BaseModel";
|
|
5
|
+
import { UniqueConstraint } from "../../schema/types/constraints/UniqueConstraint";
|
|
6
|
+
|
|
7
|
+
export const createUniqueConstraintSql = (
|
|
8
|
+
model: BaseModel,
|
|
9
|
+
constraint: UniqueConstraint,
|
|
10
|
+
) => {
|
|
11
|
+
const statement = new SQLStatement();
|
|
12
|
+
statement.push(
|
|
13
|
+
pgfmt(`CREATE UNIQUE INDEX ON %I.%I (`, model.schema, model.table),
|
|
14
|
+
);
|
|
15
|
+
statement.push(constraint.columns.map((c) => pgfmt("%I", c)).join(", "));
|
|
16
|
+
statement.push(")");
|
|
17
|
+
if (constraint.nullsNotDistinct) {
|
|
18
|
+
statement.push(" NULLS NOT DISTINCT");
|
|
19
|
+
}
|
|
20
|
+
if (constraint.where) {
|
|
21
|
+
statement.push(" WHERE (");
|
|
22
|
+
statement.push(constraint.where);
|
|
23
|
+
statement.push(")");
|
|
24
|
+
}
|
|
25
|
+
statement.push(";");
|
|
26
|
+
return statement;
|
|
27
|
+
};
|