@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.
@@ -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
+ }