@cedarjs/record 0.0.4
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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/cjs/errors.js +75 -0
- package/dist/cjs/index.js +54 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/redwoodrecord/Core.js +265 -0
- package/dist/cjs/redwoodrecord/RedwoodRecord.js +65 -0
- package/dist/cjs/redwoodrecord/Reflection.js +100 -0
- package/dist/cjs/redwoodrecord/RelationProxy.js +138 -0
- package/dist/cjs/redwoodrecord/ValidationMixin.js +92 -0
- package/dist/cjs/tasks/parse.js +121 -0
- package/dist/errors.js +47 -0
- package/dist/index.js +14 -0
- package/dist/redwoodrecord/Core.js +236 -0
- package/dist/redwoodrecord/RedwoodRecord.js +36 -0
- package/dist/redwoodrecord/Reflection.js +81 -0
- package/dist/redwoodrecord/RelationProxy.js +109 -0
- package/dist/redwoodrecord/ValidationMixin.js +73 -0
- package/dist/tasks/parse.js +88 -0
- package/package.json +49 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
class Reflection {
|
|
2
|
+
#hasMany = null;
|
|
3
|
+
#belongsTo = null;
|
|
4
|
+
#attributes = null;
|
|
5
|
+
constructor(model) {
|
|
6
|
+
this.model = model;
|
|
7
|
+
}
|
|
8
|
+
get attributes() {
|
|
9
|
+
if (!this.#attributes) {
|
|
10
|
+
this.#parseAttributes();
|
|
11
|
+
}
|
|
12
|
+
return this.#attributes;
|
|
13
|
+
}
|
|
14
|
+
get belongsTo() {
|
|
15
|
+
if (!this.#belongsTo) {
|
|
16
|
+
this.#parseBelongsTo();
|
|
17
|
+
}
|
|
18
|
+
return this.#belongsTo;
|
|
19
|
+
}
|
|
20
|
+
get hasMany() {
|
|
21
|
+
if (!this.#hasMany) {
|
|
22
|
+
this.#parseHasMany();
|
|
23
|
+
}
|
|
24
|
+
return this.#hasMany;
|
|
25
|
+
}
|
|
26
|
+
// Finds the schema for a single model
|
|
27
|
+
#schemaForModel(name = this.model.name) {
|
|
28
|
+
return this.model.schema.models.find((model) => model.name === name);
|
|
29
|
+
}
|
|
30
|
+
#parseHasMany() {
|
|
31
|
+
const selfSchema = this.#schemaForModel();
|
|
32
|
+
this.#hasMany = {};
|
|
33
|
+
selfSchema?.fields?.forEach((field) => {
|
|
34
|
+
if (field.isList) {
|
|
35
|
+
const otherSchema = this.#schemaForModel(field.type);
|
|
36
|
+
const belongsTo = otherSchema.fields.find(
|
|
37
|
+
(field2) => field2.type === this.model.name
|
|
38
|
+
);
|
|
39
|
+
this.#hasMany[field.name] = {
|
|
40
|
+
modelName: field.type,
|
|
41
|
+
referenceName: belongsTo.name,
|
|
42
|
+
// a null foreign key denotes an implicit many-to-many relationship
|
|
43
|
+
foreignKey: belongsTo.relationFromFields[0] || null,
|
|
44
|
+
primaryKey: belongsTo.relationToFields[0]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
#parseBelongsTo() {
|
|
50
|
+
const selfSchema = this.#schemaForModel();
|
|
51
|
+
this.#belongsTo = {};
|
|
52
|
+
selfSchema?.fields?.forEach((field) => {
|
|
53
|
+
if (field.relationFromFields?.length) {
|
|
54
|
+
this.#belongsTo[field.name] = {
|
|
55
|
+
modelName: field.type,
|
|
56
|
+
foreignKey: field.relationFromFields[0],
|
|
57
|
+
primaryKey: field.relationToFields[0]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
#parseAttributes() {
|
|
63
|
+
const selfSchema = this.#schemaForModel();
|
|
64
|
+
this.#attributes = {};
|
|
65
|
+
if (!this.#hasMany) {
|
|
66
|
+
this.#parseHasMany();
|
|
67
|
+
}
|
|
68
|
+
if (!this.belongsTo) {
|
|
69
|
+
this.#parseBelongsTo();
|
|
70
|
+
}
|
|
71
|
+
selfSchema?.fields?.forEach((field) => {
|
|
72
|
+
const { name, ...props } = field;
|
|
73
|
+
if (!Object.keys(this.#hasMany).includes(name) && !Object.keys(this.#belongsTo).includes(name)) {
|
|
74
|
+
this.#attributes[name] = props;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
Reflection as default
|
|
81
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import Reflection from "./Reflection";
|
|
2
|
+
class RelationProxy {
|
|
3
|
+
static addRelations(record) {
|
|
4
|
+
const reflection = new Reflection(record.constructor);
|
|
5
|
+
this.#addHasManyRelations(record, reflection.hasMany);
|
|
6
|
+
this.#addBelongsToRelations(record, reflection.belongsTo);
|
|
7
|
+
}
|
|
8
|
+
static #addHasManyRelations(record, hasMany) {
|
|
9
|
+
for (const [name, options] of Object.entries(hasMany)) {
|
|
10
|
+
if (record.hasOwnProperty(name)) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const model = record.constructor.requiredModels.find((requiredModel) => {
|
|
14
|
+
return requiredModel.name === options.modelName;
|
|
15
|
+
});
|
|
16
|
+
if (!model) {
|
|
17
|
+
console.warn(
|
|
18
|
+
`Model ${record.constructor.name} has a relationship defined for \`${name}\` in schema.prisma, but there is no Redwoood model for this relationship.`
|
|
19
|
+
);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(record, name, {
|
|
23
|
+
get() {
|
|
24
|
+
if (options.foreignKey === null) {
|
|
25
|
+
return new RelationProxy(model, {
|
|
26
|
+
where: {
|
|
27
|
+
[options.referenceName]: {
|
|
28
|
+
some: { [options.primaryKey]: record[options.primaryKey] }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
create: {
|
|
32
|
+
[options.referenceName]: {
|
|
33
|
+
connect: [
|
|
34
|
+
{ [options.primaryKey]: record[options.primaryKey] }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
return new RelationProxy(model, {
|
|
41
|
+
where: { [options.foreignKey]: record[options.primaryKey] }
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
enumerable: true
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
static #addBelongsToRelations(record, belongsTo) {
|
|
50
|
+
for (const [name, options] of Object.entries(belongsTo)) {
|
|
51
|
+
if (record.hasOwnProperty(name)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const model = record.constructor.requiredModels.find((requiredModel) => {
|
|
55
|
+
return requiredModel.name === options.modelName;
|
|
56
|
+
});
|
|
57
|
+
if (!model) {
|
|
58
|
+
console.warn(
|
|
59
|
+
`Model ${record.constructor.name} has a relationship defined for \`${name}\` in schema.prisma, but there is no Redwoood model for this relationship.`
|
|
60
|
+
);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
Object.defineProperty(record, name, {
|
|
64
|
+
async get() {
|
|
65
|
+
return await model.findBy({
|
|
66
|
+
[options.primaryKey]: record[options.foreignKey]
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
enumerable: true
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
constructor(model, relation) {
|
|
74
|
+
this.model = model;
|
|
75
|
+
this.relation = relation;
|
|
76
|
+
}
|
|
77
|
+
all(...args) {
|
|
78
|
+
return this.where(...args);
|
|
79
|
+
}
|
|
80
|
+
create(attributes, options = {}) {
|
|
81
|
+
let relatedAttributes = { ...attributes };
|
|
82
|
+
if (this.relation.create) {
|
|
83
|
+
relatedAttributes = { ...relatedAttributes, ...this.relation.create };
|
|
84
|
+
} else {
|
|
85
|
+
relatedAttributes = { ...relatedAttributes, ...this.relation.where };
|
|
86
|
+
}
|
|
87
|
+
return this.model.create(relatedAttributes, options);
|
|
88
|
+
}
|
|
89
|
+
find(id, options = {}) {
|
|
90
|
+
return this.findBy({ [this.model.primaryKey]: id }, options);
|
|
91
|
+
}
|
|
92
|
+
findBy(attributes, options = {}) {
|
|
93
|
+
const relatedAttributes = {
|
|
94
|
+
...attributes,
|
|
95
|
+
...this.relation.where
|
|
96
|
+
};
|
|
97
|
+
return this.model.findBy(relatedAttributes, options);
|
|
98
|
+
}
|
|
99
|
+
first(...args) {
|
|
100
|
+
return this.findBy(...args);
|
|
101
|
+
}
|
|
102
|
+
where(attributes, options = {}) {
|
|
103
|
+
const relatedAttributes = { ...attributes, ...this.relation.where };
|
|
104
|
+
return this.model.where(relatedAttributes, options);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
RelationProxy as default
|
|
109
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { validate as validateField } from "@cedarjs/api";
|
|
2
|
+
var ValidationMixin_default = (Base) => class extends Base {
|
|
3
|
+
// Stores error messages internally
|
|
4
|
+
_errors = { base: [] };
|
|
5
|
+
// Removes all error messages.
|
|
6
|
+
_clearErrors() {
|
|
7
|
+
for (const [attribute, _array] of Object.entries(this._errors)) {
|
|
8
|
+
this._errors[attribute] = [];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// Denotes validations that need to run for the given fields. Must be in the
|
|
12
|
+
// form of { field: options } where `field` is the name of the field and
|
|
13
|
+
// `options` are the validation options. See Service Validations docs for
|
|
14
|
+
// usage examples: https://redwoodjs.com/docs/services.html#service-validations
|
|
15
|
+
//
|
|
16
|
+
// static validates = {
|
|
17
|
+
// emailAddress: { email: true },
|
|
18
|
+
// name: { presence: true, length: { min: 2, max: 255 } }
|
|
19
|
+
// }
|
|
20
|
+
static validates = {};
|
|
21
|
+
// Whether or not this instance is valid and has no errors. Essentially the
|
|
22
|
+
// opposite of `hasError`, but runs validations first. This means it will
|
|
23
|
+
// reset any custom errors added with `addError()`
|
|
24
|
+
get isValid() {
|
|
25
|
+
this.validate();
|
|
26
|
+
return !this.hasError;
|
|
27
|
+
}
|
|
28
|
+
get errors() {
|
|
29
|
+
return this._errors;
|
|
30
|
+
}
|
|
31
|
+
// Whether or not this instance contains any errors according to validation
|
|
32
|
+
// rules. Does not run valiations, (and so preserves custom errors) returns
|
|
33
|
+
// the state of error objects. Essentially the opposite of `isValid`.
|
|
34
|
+
get hasError() {
|
|
35
|
+
return !Object.entries(this._errors).every(
|
|
36
|
+
([_name, errors]) => !errors.length
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
// Adds an error to the _errors object. Can be called manually via instance,
|
|
40
|
+
// however any errors added this way will be wiped out if calling `validate()`
|
|
41
|
+
addError(attribute, message) {
|
|
42
|
+
if (!this._errors[attribute]) {
|
|
43
|
+
this._errors[attribute] = [];
|
|
44
|
+
}
|
|
45
|
+
this._errors[attribute].push(message);
|
|
46
|
+
}
|
|
47
|
+
// Checks each field against validate directives. Creates errors if so and
|
|
48
|
+
// returns `false`, otherwise returns `true`.
|
|
49
|
+
validate(options = {}) {
|
|
50
|
+
this._clearErrors();
|
|
51
|
+
if (this.constructor.validates.length === 0) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const results = [];
|
|
55
|
+
for (const [name, recipe] of Object.entries(this.constructor.validates)) {
|
|
56
|
+
try {
|
|
57
|
+
validateField(this[name], name, recipe);
|
|
58
|
+
results.push(true);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
this.addError(name, e.message);
|
|
61
|
+
if (options.throw) {
|
|
62
|
+
throw e;
|
|
63
|
+
} else {
|
|
64
|
+
results.push(false);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return results.every((result) => result);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
export {
|
|
72
|
+
ValidationMixin_default as default
|
|
73
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getDMMF, getSchema } from "@prisma/internals";
|
|
4
|
+
import * as esbuild from "esbuild";
|
|
5
|
+
import { getPaths } from "@cedarjs/project-config";
|
|
6
|
+
const DATAMODEL_PATH = path.join(getPaths().api.models, "datamodel.js");
|
|
7
|
+
const MODELS_PATH = path.join(getPaths().api.src, "models");
|
|
8
|
+
const MODELS_INDEX_PATH = path.join(MODELS_PATH, "index.js");
|
|
9
|
+
const indexLines = [
|
|
10
|
+
"// This file is autogenerated by Redwood and will be overwritten periodically",
|
|
11
|
+
"",
|
|
12
|
+
"import { db } from 'src/lib/db'",
|
|
13
|
+
"import datamodel from 'src/models/datamodel'",
|
|
14
|
+
"import { RedwoodRecord } from '@cedarjs/record'",
|
|
15
|
+
"",
|
|
16
|
+
"RedwoodRecord.db = db",
|
|
17
|
+
"RedwoodRecord.schema = datamodel",
|
|
18
|
+
""
|
|
19
|
+
];
|
|
20
|
+
const modelImports = [];
|
|
21
|
+
const modelRequires = {};
|
|
22
|
+
let datamodel;
|
|
23
|
+
const parseDatamodel = () => {
|
|
24
|
+
const schema = getSchema(getPaths().api.dbSchema);
|
|
25
|
+
getDMMF({ datamodel: schema }).then((schema2) => {
|
|
26
|
+
datamodel = schema2.datamodel;
|
|
27
|
+
try {
|
|
28
|
+
const dir = path.dirname(DATAMODEL_PATH);
|
|
29
|
+
if (!fs.existsSync(dir)) {
|
|
30
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
DATAMODEL_PATH,
|
|
34
|
+
esbuild.transformSync(JSON.stringify(datamodel, null, 2), {
|
|
35
|
+
loader: "json",
|
|
36
|
+
format: "cjs"
|
|
37
|
+
}).code
|
|
38
|
+
);
|
|
39
|
+
console.info(`
|
|
40
|
+
Wrote ${DATAMODEL_PATH}`);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error("Error writing datamodel to", DATAMODEL_PATH);
|
|
43
|
+
}
|
|
44
|
+
const modelNames = fs.readdirSync(MODELS_PATH).map((file) => {
|
|
45
|
+
if (file !== "index.js" && file !== "datamodel.js") {
|
|
46
|
+
return file.split(".")[0];
|
|
47
|
+
}
|
|
48
|
+
}).filter((val) => val);
|
|
49
|
+
if (modelNames.length === 0) {
|
|
50
|
+
console.warn("No models found in", MODELS_PATH);
|
|
51
|
+
console.warn(
|
|
52
|
+
"Please create a model to represent the database table you want to access."
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
modelNames.forEach((modelName) => {
|
|
57
|
+
const thisModelRequires = [];
|
|
58
|
+
modelImports.push(`import ${modelName} from 'src/models/${modelName}'`);
|
|
59
|
+
const schemaModel = datamodel.models.find(
|
|
60
|
+
(model) => model.name === modelName
|
|
61
|
+
);
|
|
62
|
+
if (schemaModel) {
|
|
63
|
+
schemaModel.fields.forEach((field) => {
|
|
64
|
+
if (field.kind === "object" && modelNames.includes(field.type)) {
|
|
65
|
+
thisModelRequires.push(field.type);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
modelRequires[modelName] = thisModelRequires;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
modelImports.forEach((modelImport) => {
|
|
72
|
+
indexLines.push(modelImport);
|
|
73
|
+
});
|
|
74
|
+
indexLines.push("");
|
|
75
|
+
for (const [name, requires] of Object.entries(modelRequires)) {
|
|
76
|
+
indexLines.push(`${name}.requiredModels = [${requires.join(", ")}]`);
|
|
77
|
+
}
|
|
78
|
+
indexLines.push("");
|
|
79
|
+
indexLines.push(`export { ${modelNames.join(", ")} }`);
|
|
80
|
+
indexLines.push("");
|
|
81
|
+
fs.writeFileSync(MODELS_INDEX_PATH, indexLines.join("\n"));
|
|
82
|
+
console.info(` Wrote ${MODELS_INDEX_PATH}
|
|
83
|
+
`);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
export {
|
|
87
|
+
parseDatamodel
|
|
88
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cedarjs/record",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/cedarjs/cedar.git",
|
|
7
|
+
"directory": "packages/record"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/cjs/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/cjs/index.js",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsx ./build.mts",
|
|
24
|
+
"build:pack": "yarn pack -o cedar-record.tgz",
|
|
25
|
+
"build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"",
|
|
26
|
+
"check:package": "yarn publint",
|
|
27
|
+
"prepublishOnly": "NODE_ENV=production yarn build",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest watch"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@cedarjs/api": "0.0.4",
|
|
33
|
+
"@cedarjs/project-config": "0.0.4",
|
|
34
|
+
"@prisma/client": "5.20.0",
|
|
35
|
+
"camelcase": "6.3.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@cedarjs/framework-tools": "0.0.4",
|
|
39
|
+
"@prisma/internals": "5.20.0",
|
|
40
|
+
"esbuild": "0.25.0",
|
|
41
|
+
"publint": "0.3.11",
|
|
42
|
+
"tsx": "4.19.3",
|
|
43
|
+
"vitest": "2.1.9"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"gitHead": "5b4f77f985bd86ee31ee7338312627accf0cb85b"
|
|
49
|
+
}
|